1import logging
2import random
3import re
4from io import BytesIO
5from urllib.parse import parse_qsl, urlparse
6
7from streamlink import PluginError
8from streamlink.packages.flashmedia import AMFMessage, AMFPacket
9from streamlink.packages.flashmedia.types import AMF3ObjectBase
10from streamlink.plugin import Plugin
11from streamlink.plugin.api import useragents, validate
12from streamlink.stream import HLSStream, HTTPStream, RTMPStream
13
14log = logging.getLogger(__name__)
15
16
17@AMF3ObjectBase.register("com.brightcove.experience.ViewerExperienceRequest")
18class ViewerExperienceRequest(AMF3ObjectBase):
19    def __init__(self, experienceId, URL, playerKey, deliveryType, TTLToken, contentOverrides):
20        self.experienceId = experienceId
21        self.URL = URL
22        self.playerKey = playerKey
23        self.deliveryType = deliveryType
24        self.TTLToken = TTLToken
25        self.contentOverrides = contentOverrides
26
27
28@AMF3ObjectBase.register("com.brightcove.experience.ContentOverride")
29class ContentOverride(AMF3ObjectBase):
30    def __init__(self, featuredRefId, contentRefIds, contentId, target="videoPlayer", contentIds=None,
31                 contentType=0, featuredId=float('nan'), contentRefId=None):
32        self.featuredRefId = featuredRefId
33        self.contentRefIds = contentRefIds
34        self.contentId = contentId
35        self.target = target
36        self.contentIds = contentIds
37        self.contentType = contentType
38        self.featuredId = featuredId
39        self.contentRefId = contentRefId
40
41
42class BrightcovePlayer:
43    player_page = "http://players.brightcove.net/{account_id}/{player_id}/index.html"
44    api_url = "https://edge.api.brightcove.com/playback/v1/"
45    amf_broker = "http://c.brightcove.com/services/messagebroker/amf"
46
47    policy_key_re = re.compile(r'''policyKey\s*:\s*(?P<q>['"])(?P<key>.*?)(?P=q)''')
48
49    schema = validate.Schema({
50        "sources": [{
51            validate.optional("height"): validate.any(int, None),
52            validate.optional("avg_bitrate"): validate.any(int, None),
53            validate.optional("src"): validate.url(),
54            validate.optional("app_name"): validate.any(
55                validate.url(scheme="rtmp"),
56                validate.url(scheme="rtmpe")
57            ),
58            validate.optional("stream_name"): validate.text,
59            validate.optional("type"): validate.text
60        }]
61    })
62
63    def __init__(self, session, account_id, player_id="default_default"):
64        self.session = session
65        log.debug("Creating player for account {0} (player_id={1})".format(account_id, player_id))
66        self.account_id = account_id
67        self.player_id = player_id
68
69    def player_url(self, video_id):
70        return self.player_page.format(account_id=self.account_id,
71                                       player_id=self.player_id,
72                                       params=dict(videoId=video_id))
73
74    def video_info(self, video_id, policy_key):
75        url = "{base}accounts/{account_id}/videos/{video_id}".format(base=self.api_url,
76                                                                     account_id=self.account_id,
77                                                                     video_id=video_id)
78        res = self.session.http.get(
79            url,
80            headers={
81                "User-Agent": useragents.CHROME,
82                "Referer": self.player_url(video_id),
83                "Accept": "application/json;pk={0}".format(policy_key)
84            }
85        )
86        return self.session.http.json(res, schema=self.schema)
87
88    def policy_key(self, video_id):
89        # Get the embedded player page
90        res = self.session.http.get(self.player_url(video_id))
91
92        policy_key_m = self.policy_key_re.search(res.text)
93        policy_key = policy_key_m and policy_key_m.group("key")
94        if not policy_key:
95            raise PluginError("Could not find Brightcove policy key")
96
97        return policy_key
98
99    def get_streams(self, video_id):
100        log.debug("Finding streams for video: {0}".format(video_id))
101        policy_key = self.policy_key(video_id)
102        log.debug("Found policy key: {0}".format(policy_key))
103        data = self.video_info(video_id, policy_key)
104        headers = {"Referer": self.player_url(video_id)}
105
106        for source in data.get("sources"):
107            # determine quality name
108            if source.get("height"):
109                q = "{0}p".format(source.get("height"))
110            elif source.get("avg_bitrate"):
111                q = "{0}k".format(source.get("avg_bitrate") // 1000)
112            else:
113                q = "live"
114            if ((source.get("type") == "application/x-mpegURL" and source.get("src"))
115                    or (source.get("src") and ".m3u8" in source.get("src"))):
116                yield from HLSStream.parse_variant_playlist(self.session, source.get("src"), headers=headers).items()
117            elif source.get("app_name"):
118                s = RTMPStream(self.session,
119                               {"rtmp": source.get("app_name"),
120                                "playpath": source.get("stream_name")})
121                yield q, s
122            elif source.get("src") and source.get("src").endswith(".mp4"):
123                yield q, HTTPStream(self.session, source.get("src"), headers=headers)
124
125    @classmethod
126    def from_url(cls, session, url):
127        purl = urlparse(url)
128        querys = dict(parse_qsl(purl.query))
129
130        account_id, player_id, _ = purl.path.lstrip("/").split("/", 3)
131        video_id = querys.get("videoId")
132
133        bp = cls(session, account_id=account_id, player_id=player_id)
134        return bp.get_streams(video_id)
135
136    @classmethod
137    def from_player_key(cls, session, player_id, player_key, video_id, url=None):
138        amf_message = AMFMessage("com.brightcove.experience.ExperienceRuntimeFacade.getDataForExperience",
139                                 "/1",
140                                 [
141                                     ''.join(["{0:02x}".format(random.randint(0, 255)) for _ in range(20)]),  # random id
142                                     ViewerExperienceRequest(experienceId=int(player_id),
143                                                             URL=url or "",
144                                                             playerKey=player_key,
145                                                             deliveryType=float('nan'),
146                                                             TTLToken="",
147                                                             contentOverrides=[ContentOverride(
148                                                                 featuredRefId=None,
149                                                                 contentRefIds=None,
150                                                                 contentId=int(video_id)
151                                                             )])
152                                 ])
153        amf_packet = AMFPacket(version=3)
154        amf_packet.messages.append(amf_message)
155
156        res = session.http.post(
157            cls.amf_broker,
158            headers={"Content-Type": "application/x-amf"},
159            data=amf_packet.serialize(),
160            params=dict(playerKey=player_key),
161            raise_for_status=False
162        )
163        data = AMFPacket.deserialize(BytesIO(res.content))
164        result = data.messages[0].value
165        bp = cls(session=session, account_id=int(result.publisherId))
166        return bp.get_streams(video_id)
167
168
169class Brightcove(Plugin):
170    url_re = re.compile(r"https?://players\.brightcove\.net/.*?/index.html")
171
172    @classmethod
173    def can_handle_url(cls, url):
174        return cls.url_re.match(url) is not None
175
176    def _get_streams(self):
177        return BrightcovePlayer.from_url(self.session, self.url)
178
179
180__plugin__ = Brightcove
181