1import re 2 3from io import BytesIO 4from time import sleep 5 6from livestreamer.exceptions import PluginError 7from livestreamer.packages.flashmedia import AMFPacket, AMFMessage 8from livestreamer.packages.flashmedia.types import AMF3ObjectBase 9from livestreamer.plugin import Plugin 10from livestreamer.plugin.api import http, validate 11from livestreamer.stream import AkamaiHDStream 12 13AMF_GATEWAY = "http://c.brightcove.com/services/messagebroker/amf" 14AMF_MESSAGE_PREFIX = "af6b88c640c8d7b4cc75d22f7082ad95603bc627" 15STREAM_NAMES = ["360p", "480p", "720p", "source"] 16HTTP_HEADERS = { 17 "User-Agent": ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " 18 "(KHTML, like Gecko) Chrome/36.0.1944.9 Safari/537.36") 19} 20 21_url_re = re.compile("http(s)?://(\w+\.)?azubu.tv/[^/]+") 22 23_viewerexp_schema = validate.Schema( 24 validate.attr({ 25 "programmedContent": { 26 "videoPlayer": validate.attr({ 27 "mediaDTO": validate.attr({ 28 "renditions": { 29 int: validate.attr({ 30 "encodingRate": int, 31 "defaultURL": validate.text 32 }) 33 } 34 }) 35 }) 36 } 37 }) 38) 39 40 41@AMF3ObjectBase.register("com.brightcove.experience.ViewerExperienceRequest") 42class ViewerExperienceRequest(AMF3ObjectBase): 43 __members__ = ["contentOverrides", 44 "experienceId", 45 "URL", 46 "playerKey", 47 "deliveryType", 48 "TTLToken"] 49 50 def __init__(self, URL, contentOverrides, experienceId, playerKey, TTLToken=""): 51 self.URL = URL 52 self.deliveryType = float("nan") 53 self.contentOverrides = contentOverrides 54 self.experienceId = experienceId 55 self.playerKey = playerKey 56 self.TTLToken = TTLToken 57 58 59@AMF3ObjectBase.register("com.brightcove.experience.ContentOverride") 60class ContentOverride(AMF3ObjectBase): 61 __members__ = ["featuredRefId", 62 "contentRefIds", 63 "contentId", 64 "contentType", 65 "contentIds", 66 "featuredId", 67 "contentRefId", 68 "target"] 69 70 def __init__(self, contentId=float("nan"), contentRefId=None, contentType=0, 71 target="videoPlayer"): 72 self.contentType = contentType 73 self.contentId = contentId 74 self.target = target 75 self.contentIds = None 76 self.contentRefId = contentRefId 77 self.contentRefIds = None 78 self.contentType = 0 79 self.featuredId = float("nan") 80 self.featuredRefId = None 81 82 83class AzubuTV(Plugin): 84 @classmethod 85 def can_handle_url(cls, url): 86 return _url_re.match(url) 87 88 @classmethod 89 def stream_weight(cls, stream): 90 if stream == "source": 91 weight = 1080 92 else: 93 weight, group = Plugin.stream_weight(stream) 94 95 return weight, "azubutv" 96 97 def _create_amf_request(self, key, video_player, player_id): 98 if video_player.startswith("ref:"): 99 content_override = ContentOverride(contentRefId=video_player[4:]) 100 else: 101 content_override = ContentOverride(contentId=int(video_player)) 102 viewer_exp_req = ViewerExperienceRequest(self.url, 103 [content_override], 104 int(player_id), key) 105 106 req = AMFPacket(version=3) 107 req.messages.append(AMFMessage( 108 "com.brightcove.experience.ExperienceRuntimeFacade.getDataForExperience", 109 "/1", 110 [AMF_MESSAGE_PREFIX, viewer_exp_req] 111 )) 112 113 return req 114 115 def _send_amf_request(self, req, key): 116 headers = { 117 "content-type": "application/x-amf" 118 } 119 res = http.post(AMF_GATEWAY, data=bytes(req.serialize()), 120 headers=headers, params=dict(playerKey=key)) 121 122 return AMFPacket.deserialize(BytesIO(res.content)) 123 124 def _get_player_params(self, retries=5): 125 try: 126 res = http.get(self.url, headers=HTTP_HEADERS) 127 except PluginError as err: 128 # The server sometimes gives us 404 for no reason 129 if "404" in str(err) and retries: 130 sleep(1) 131 return self._get_player_params(retries - 1) 132 else: 133 raise 134 135 match = re.search("<param name=\"playerKey\" value=\"(.+)\" />", res.text) 136 if not match: 137 # The HTML returned sometimes doesn't contain the parameters 138 if not retries: 139 raise PluginError("Missing key 'playerKey' in player params") 140 else: 141 sleep(1) 142 return self._get_player_params(retries - 1) 143 144 key = match.group(1) 145 match = re.search("AZUBU.setVar\(\"firstVideoRefId\", \"(.+)\"\);", res.text) 146 if not match: 147 # The HTML returned sometimes doesn't contain the parameters 148 if not retries: 149 raise PluginError("Unable to find video reference") 150 else: 151 sleep(1) 152 return self._get_player_params(retries - 1) 153 154 video_player = "ref:" + match.group(1) 155 match = re.search("<param name=\"playerID\" value=\"(\d+)\" />", res.text) 156 if not match: 157 # The HTML returned sometimes doesn't contain the parameters 158 if not retries: 159 raise PluginError("Missing key 'playerID' in player params") 160 else: 161 sleep(1) 162 return self._get_player_params(retries - 1) 163 164 player_id = match.group(1) 165 match = re.search("<!-- live on -->", res.text) 166 if not match: 167 match = re.search("<div id=\"channel_live\">", res.text) 168 is_live = not not match 169 170 return key, video_player, player_id, is_live 171 172 def _parse_result(self, res): 173 res = _viewerexp_schema.validate(res) 174 player = res.programmedContent["videoPlayer"] 175 renditions = sorted(player.mediaDTO.renditions.values(), 176 key=lambda r: r.encodingRate or 100000000) 177 178 streams = {} 179 for stream_name, rendition in zip(STREAM_NAMES, renditions): 180 stream = AkamaiHDStream(self.session, rendition.defaultURL) 181 streams[stream_name] = stream 182 183 return streams 184 185 def _get_streams(self): 186 key, video_player, player_id, is_live = self._get_player_params() 187 188 if not is_live: 189 return 190 191 req = self._create_amf_request(key, video_player, player_id) 192 res = self._send_amf_request(req, key) 193 194 streams = {} 195 for message in res.messages: 196 if message.target_uri == "/1/onResult": 197 streams = self._parse_result(message.value) 198 199 return streams 200 201__plugin__ = AzubuTV 202