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