2 from __future__ 
import unicode_literals
 
   9 from .common 
import InfoExtractor
 
  10 from .adobepass 
import AdobePassIE
 
  11 from ..compat 
import ( 
  12     compat_etree_fromstring
, 
  15     compat_urllib_parse_urlparse
, 
  17     compat_xml_parse_error
, 
  38 class BrightcoveLegacyIE(InfoExtractor
): 
  39     IE_NAME 
= 'brightcove:legacy' 
  40     _VALID_URL 
= r
'(?:https?://.*brightcove\.com/(services|viewer).*?\?|brightcove:)(?P<query>.*)' 
  41     _FEDERATED_URL 
= 'http://c.brightcove.com/services/viewer/htmlFederated' 
  45             # From http://www.8tv.cat/8aldia/videos/xavier-sala-i-martin-aquesta-tarda-a-8-al-dia/ 
  46             'url': 'http://c.brightcove.com/services/viewer/htmlFederated?playerID=1654948606001&flashID=myExperience&%40videoPlayer=2371591881001', 
  47             'md5': '5423e113865d26e40624dce2e4b45d95', 
  48             'note': 'Test Brightcove downloads and detection in GenericIE', 
  50                 'id': '2371591881001', 
  52                 'title': 'Xavier Sala i Martín: “Un banc que no presta és un banc zombi que no serveix per a res”', 
  54                 'description': 'md5:a950cc4285c43e44d763d036710cd9cd', 
  55                 'timestamp': 1368213670, 
  56                 'upload_date': '20130510', 
  57                 'uploader_id': '1589608506001', 
  61             # From http://medianetwork.oracle.com/video/player/1785452137001 
  62             'url': 'http://c.brightcove.com/services/viewer/htmlFederated?playerID=1217746023001&flashID=myPlayer&%40videoPlayer=1785452137001', 
  64                 'id': '1785452137001', 
  66                 'title': 'JVMLS 2012: Arrays 2.0 - Opportunities and Challenges', 
  67                 'description': 'John Rose speaks at the JVM Language Summit, August 1, 2012.', 
  69                 'timestamp': 1344975024, 
  70                 'upload_date': '20120814', 
  71                 'uploader_id': '1460825906', 
  75             # From http://mashable.com/2013/10/26/thermoelectric-bracelet-lets-you-control-your-body-temperature/ 
  76             'url': 'http://c.brightcove.com/services/viewer/federated_f9?&playerID=1265504713001&publisherID=AQ%7E%7E%2CAAABBzUwv1E%7E%2CxP-xFHVUstiMFlNYfvF4G9yFnNaqCw_9&videoID=2750934548001', 
  78                 'id': '2750934548001', 
  80                 'title': 'This Bracelet Acts as a Personal Thermostat', 
  81                 'description': 'md5:547b78c64f4112766ccf4e151c20b6a0', 
  82                 'uploader': 'Mashable', 
  83                 'timestamp': 1382041798, 
  84                 'upload_date': '20131017', 
  85                 'uploader_id': '1130468786001', 
  89             # test that the default referer works 
  90             # from http://national.ballet.ca/interact/video/Lost_in_Motion_II/ 
  91             'url': 'http://link.brightcove.com/services/player/bcpid756015033001?bckey=AQ~~,AAAApYJi_Ck~,GxhXCegT1Dp39ilhXuxMJxasUhVNZiil&bctid=2878862109001', 
  93                 'id': '2878862109001', 
  95                 'title': 'Lost in Motion II', 
  96                 'description': 'md5:363109c02998fee92ec02211bd8000df', 
  97                 'uploader': 'National Ballet of Canada', 
 102             # test flv videos served by akamaihd.net 
 103             # From http://www.redbull.com/en/bike/stories/1331655643987/replay-uci-dh-world-cup-2014-from-fort-william 
 104             'url': 'http://c.brightcove.com/services/viewer/htmlFederated?%40videoPlayer=ref%3Aevent-stream-356&linkBaseURL=http%3A%2F%2Fwww.redbull.com%2Fen%2Fbike%2Fvideos%2F1331655630249%2Freplay-uci-fort-william-2014-dh&playerKey=AQ%7E%7E%2CAAAApYJ7UqE%7E%2Cxqr_zXk0I-zzNndy8NlHogrCb5QdyZRf&playerID=1398061561001#__youtubedl_smuggle=%7B%22Referer%22%3A+%22http%3A%2F%2Fwww.redbull.com%2Fen%2Fbike%2Fstories%2F1331655643987%2Freplay-uci-dh-world-cup-2014-from-fort-william%22%7D', 
 105             # The md5 checksum changes on each download 
 107                 'id': '3750436379001', 
 109                 'title': 'UCI MTB World Cup 2014: Fort William, UK - Downhill Finals', 
 110                 'uploader': 'RBTV Old (do not use)', 
 111                 'description': 'UCI MTB World Cup 2014: Fort William, UK - Downhill Finals', 
 112                 'timestamp': 1409122195, 
 113                 'upload_date': '20140827', 
 114                 'uploader_id': '710858724001', 
 116             'skip': 'Video gone', 
 119             # playlist with 'videoList' 
 120             # from http://support.brightcove.com/en/video-cloud/docs/playlist-support-single-video-players 
 121             'url': 'http://c.brightcove.com/services/viewer/htmlFederated?playerID=3550052898001&playerKey=AQ%7E%7E%2CAAABmA9XpXk%7E%2C-Kp7jNgisre1fG5OdqpAFUTcs0lP_ZoL', 
 124                 'id': '3550319591001', 
 126             'playlist_mincount': 7, 
 129             # playlist with 'playlistTab' (https://github.com/rg3/youtube-dl/issues/9965) 
 130             'url': 'http://c.brightcove.com/services/json/experience/runtime/?command=get_programming_for_experience&playerKey=AQ%7E%7E,AAABXlLMdok%7E,NJ4EoMlZ4rZdx9eU1rkMVd8EaYPBBUlg', 
 132                 'id': '1522758701001', 
 133                 'title': 'Lesson 08', 
 135             'playlist_mincount': 10, 
 138             # playerID inferred from bcpid 
 139             # from http://www.un.org/chinese/News/story.asp?NewsID=27724 
 140             'url': 'https://link.brightcove.com/services/player/bcpid1722935254001/?bctid=5360463607001&autoStart=false&secureConnections=true&width=650&height=350', 
 141             'only_matching': True,  # Tested in GenericIE 
 152     def _build_brighcove_url(cls
, object_str
): 
 154         Build a Brightcove url from a xml string containing 
 155         <object class="BrightcoveExperience">{params}</object> 
 158         # Fix up some stupid HTML, see https://github.com/rg3/youtube-dl/issues/1553 
 159         object_str 
= re
.sub(r
'(<param(?:\s+[a-zA-Z0-9_]+="[^"]*")*)>', 
 160                             lambda m
: m
.group(1) + '/>', object_str
) 
 161         # Fix up some stupid XML, see https://github.com/rg3/youtube-dl/issues/1608 
 162         object_str 
= object_str
.replace('<--', '<!--') 
 163         # remove namespace to simplify extraction 
 164         object_str 
= re
.sub(r
'(<object[^>]*)(xmlns=".*?")', r
'\1', object_str
) 
 165         object_str 
= fix_xml_ampersands(object_str
) 
 168             object_doc 
= compat_etree_fromstring(object_str
.encode('utf-8')) 
 169         except compat_xml_parse_error
: 
 172         fv_el 
= find_xpath_attr(object_doc
, './param', 'name', 'flashVars') 
 173         if fv_el 
is not None: 
 176                 for k
, v 
in compat_parse_qs(fv_el
.attrib
['value']).items()) 
 180         data_url 
= object_doc
.attrib
.get('data', '') 
 181         data_url_params 
= compat_parse_qs(compat_urllib_parse_urlparse(data_url
).query
) 
 183         def find_param(name
): 
 184             if name 
in flashvars
: 
 185                 return flashvars
[name
] 
 186             node 
= find_xpath_attr(object_doc
, './param', 'name', name
) 
 188                 return node
.attrib
['value'] 
 189             return data_url_params
.get(name
) 
 193         playerID 
= find_param('playerID') or find_param('playerId') 
 195             raise ExtractorError('Cannot find player ID') 
 196         params
['playerID'] = playerID
 
 198         playerKey 
= find_param('playerKey') 
 199         # Not all pages define this value 
 200         if playerKey 
is not None: 
 201             params
['playerKey'] = playerKey
 
 202         # These fields hold the id of the video 
 203         videoPlayer 
= find_param('@videoPlayer') or find_param('videoId') or find_param('videoID') or find_param('@videoList') 
 204         if videoPlayer 
is not None: 
 205             if isinstance(videoPlayer
, list): 
 206                 videoPlayer 
= videoPlayer
[0] 
 207             videoPlayer 
= videoPlayer
.strip() 
 208             # UUID is also possible for videoPlayer (e.g. 
 209             # http://www.popcornflix.com/hoodies-vs-hooligans/7f2d2b87-bbf2-4623-acfb-ea942b4f01dd 
 210             # or http://www8.hp.com/cn/zh/home.html) 
 212                     r
'^(?:\d+|[\da-fA-F]{8}-?[\da-fA-F]{4}-?[\da-fA-F]{4}-?[\da-fA-F]{4}-?[\da-fA-F]{12})$', 
 213                     videoPlayer
) or videoPlayer
.startswith('ref:')): 
 215             params
['@videoPlayer'] = videoPlayer
 
 216         linkBase 
= find_param('linkBaseURL') 
 217         if linkBase 
is not None: 
 218             params
['linkBaseURL'] = linkBase
 
 219         return cls
._make
_brightcove
_url
(params
) 
 222     def _build_brighcove_url_from_js(cls
, object_js
): 
 223         # The layout of JS is as follows: 
 224         # customBC.createVideo = function (width, height, playerID, playerKey, videoPlayer, VideoRandomID) { 
 225         #   // build Brightcove <object /> XML 
 228             r
'''(?x)customBC\.createVideo\( 
 229                 .*?                                                  # skipping width and height 
 230                 ["\'](?P
<playerID
>\d
+)["\']\s*,\s*                   # playerID 
 231                 ["\'](?P
<playerKey
>AQ
[^
"\']{48})[^"\']*["\']\s*,\s*  # playerKey begins with AQ and is 50 characters 
 232                                                                      # in length, however it's appended to itself 
 233                                                                      # in places, so truncate 
 234                 ["\'](?P
<videoID
>\d
+)["\']                           # @videoPlayer 
 237             return cls._make_brightcove_url(m.groupdict()) 
 240     def _make_brightcove_url(cls, params): 
 241         return update_url_query(cls._FEDERATED_URL, params) 
 244     def _extract_brightcove_url(cls, webpage): 
 245         """Try to extract the brightcove url from the webpage, returns None 
 248         urls = cls._extract_brightcove_urls(webpage) 
 249         return urls[0] if urls else None 
 252     def _extract_brightcove_urls(cls, webpage): 
 253         """Return a list of all Brightcove URLs from the webpage """ 
 258                     (?:property|itemprop)=([\'"])(?
:og
:video|embedURL
)\
1[^
>]+ 
 259                     content
=([\'"])(?P<url>https?://(?:secure|c)\.brightcove.com/(?:(?!\2).)+)\2 
 262             url = unescapeHTML(url_m.group('url')) 
 263             # Some sites don't add it, we can't download with this url, for example: 
 264             # http://www.ktvu.com/videos/news/raw-video-caltrain-releases-video-of-man-almost/vCTZdY/ 
 265             if 'playerKey' in url or 'videoId' in url or 'idVideo' in url: 
 268         matches = re.findall( 
 271                 [^>]+?class=[\'"][^
>]*?BrightcoveExperience
.*?
[\'"] | 
 272                 [^>]*?>\s*<param\s+name="movie
"\s+value="https?
://[^
/]*brightcove\
.com
/ 
 273             ).+?
>\s
*</object>''', 
 276             return list(filter(None, [cls._build_brighcove_url(m) for m in matches])) 
 278         matches = re.findall(r'(customBC\.createVideo\(.+?\);)', webpage) 
 280             return list(filter(None, [ 
 281                 cls._build_brighcove_url_from_js(custom_bc) 
 282                 for custom_bc in matches])) 
 283         return [src for _, src in re.findall( 
 284             r'<iframe[^>]+src=([\'"])((?:https?:)?//link\.brightcove\.com/services/player/(?!\1).+)\1', webpage)] 
 286     def _real_extract(self, url): 
 287         url, smuggled_data = unsmuggle_url(url, {}) 
 289         # Change the 'videoId' and others field to '@videoPlayer' 
 290         url = re.sub(r'(?<=[?&])(videoI(d|D)|idVideo|bctid)', '%40videoPlayer', url) 
 291         # Change bckey (used by bcove.me urls) to playerKey 
 292         url = re.sub(r'(?<=[?&])bckey', 'playerKey', url) 
 293         mobj = re.match(self._VALID_URL, url) 
 294         query_str = mobj.group('query') 
 295         query = compat_urlparse.parse_qs(query_str) 
 297         videoPlayer = query.get('@videoPlayer') 
 299             # We set the original url as the default 'Referer' header 
 300             referer = smuggled_data.get('Referer', url) 
 301             if 'playerID' not in query: 
 302                 mobj = re.search(r'/bcpid(\d+)', url) 
 304                     query['playerID'] = [mobj.group(1)] 
 305             return self._get_video_info( 
 306                 videoPlayer[0], query, referer=referer) 
 307         elif 'playerKey' in query: 
 308             player_key = query['playerKey'] 
 309             return self._get_playlist_info(player_key[0]) 
 311             raise ExtractorError( 
 312                 'Cannot find playerKey= variable. Did you forget quotes in a shell invocation?', 
 315     def _brightcove_new_url_result(self, publisher_id, video_id): 
 316         brightcove_new_url = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s' % (publisher_id, video_id) 
 317         return self.url_result(brightcove_new_url, BrightcoveNewIE.ie_key(), video_id) 
 319     def _get_video_info(self, video_id, query, referer=None): 
 321         linkBase = query.get('linkBaseURL') 
 322         if linkBase is not None: 
 323             referer = linkBase[0] 
 324         if referer is not None: 
 325             headers['Referer'] = referer 
 326         webpage = self._download_webpage(self._FEDERATED_URL, video_id, headers=headers, query=query) 
 328         error_msg = self._html_search_regex( 
 329             r"<h1>We're sorry.</h1>([\s\n]*<p>.*?</p>)+", webpage, 
 330             'error message', default=None) 
 331         if error_msg is not None: 
 332             publisher_id = query.get('publisherId') 
 333             if publisher_id and publisher_id[0].isdigit(): 
 334                 publisher_id = publisher_id[0] 
 336                 player_key = query.get('playerKey') 
 337                 if player_key and ',' in player_key[0]: 
 338                     player_key = player_key[0] 
 340                     player_id = query.get('playerID') 
 341                     if player_id and player_id[0].isdigit(): 
 342                         player_page = self._download_webpage( 
 343                             'http://link.brightcove.com/services/player/bcpid' + player_id[0], 
 344                             video_id, headers=headers, fatal=False) 
 346                             player_key = self._search_regex( 
 347                                 r'<param\s+name="playerKey"\s+value="([\w~,-]+)"', 
 348                                 player_page, 'player key', fatal=False) 
 350                     enc_pub_id = player_key.split(',')[1].replace('~', '=') 
 351                     publisher_id = struct.unpack('>Q', base64.urlsafe_b64decode(enc_pub_id))[0] 
 353                     return self._brightcove_new_url_result(publisher_id, video_id) 
 354             raise ExtractorError( 
 355                 'brightcove said: %s' % error_msg, expected=True) 
 357         self.report_extraction(video_id) 
 358         info = self._search_regex(r'var experienceJSON = ({.*});', webpage, 'json') 
 359         info = json.loads(info)['data'] 
 360         video_info = info['programmedContent']['videoPlayer']['mediaDTO'] 
 361         video_info['_youtubedl_adServerURL'] = info.get('adServerURL') 
 363         return self._extract_video_info(video_info) 
 365     def _get_playlist_info(self, player_key): 
 366         info_url = 'http://c.brightcove.com/services/json/experience/runtime/?command=get_programming_for_experience&playerKey=%s' % player_key 
 367         playlist_info = self._download_webpage( 
 368             info_url, player_key, 'Downloading playlist information') 
 370         json_data = json.loads(playlist_info) 
 371         if 'videoList' in json_data: 
 372             playlist_info = json_data['videoList'] 
 373             playlist_dto = playlist_info['mediaCollectionDTO'] 
 374         elif 'playlistTabs' in json_data: 
 375             playlist_info = json_data['playlistTabs'] 
 376             playlist_dto = playlist_info['lineupListDTO']['playlistDTOs'][0] 
 378             raise ExtractorError('Empty playlist') 
 380         videos = [self._extract_video_info(video_info) for video_info in playlist_dto['videoDTOs']] 
 382         return self.playlist_result(videos, playlist_id='%s' % playlist_info['id'], 
 383                                     playlist_title=playlist_dto['displayName']) 
 385     def _extract_video_info(self, video_info): 
 386         video_id = compat_str(video_info['id']) 
 387         publisher_id = video_info.get('publisherId') 
 390             'title': video_info['displayName'].strip(), 
 391             'description': video_info.get('shortDescription'), 
 392             'thumbnail': video_info.get('videoStillURL') or video_info.get('thumbnailURL'), 
 393             'uploader': video_info.get('publisherName'), 
 394             'uploader_id': compat_str(publisher_id) if publisher_id else None, 
 395             'duration': float_or_none(video_info.get('length'), 1000), 
 396             'timestamp': int_or_none(video_info.get('creationDate'), 1000), 
 399         renditions = video_info.get('renditions', []) + video_info.get('IOSRenditions', []) 
 402             for rend in renditions: 
 403                 url = rend['defaultURL'] 
 408                     url_comp = compat_urllib_parse_urlparse(url) 
 409                     if url_comp.path.endswith('.m3u8'): 
 411                             self._extract_m3u8_formats( 
 412                                 url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False)) 
 414                     elif 'akamaihd.net' in url_comp.netloc: 
 415                         # This type of renditions are served through 
 416                         # akamaihd.net, but they don't use f4m manifests 
 417                         url = url.replace('control/', '') + '?&v=3.3.0&fp=13&r=FEEFJ&g=RTSJIMBMPFPB' 
 420                     ext = determine_ext(url) 
 421                 tbr = int_or_none(rend.get('encodingRate'), 1000) 
 423                     'format_id': 'http%s' % ('-%s' % tbr if tbr else ''), 
 426                     'filesize': int_or_none(rend.get('size')) or None, 
 429                 if rend.get('audioOnly'): 
 435                         'height': int_or_none(rend.get('frameHeight')), 
 436                         'width': int_or_none(rend.get('frameWidth')), 
 437                         'vcodec': rend.get('videoCodec'), 
 440                 # m3u8 manifests with remote == false are media playlists 
 441                 # Not calling _extract_m3u8_formats here to save network traffic 
 444                         'format_id': 'hls%s' % ('-%s' % tbr if tbr else ''), 
 446                         'protocol': 'm3u8_native', 
 449                 formats.append(a_format) 
 450             self._sort_formats(formats) 
 451             info['formats'] = formats 
 452         elif video_info.get('FLVFullLengthURL') is not None: 
 454                 'url': video_info['FLVFullLengthURL'], 
 455                 'vcodec': self.FLV_VCODECS.get(video_info.get('FLVFullCodec')), 
 456                 'filesize': int_or_none(video_info.get('FLVFullSize')), 
 459         if self._downloader.params.get('include_ads', False): 
 460             adServerURL = video_info.get('_youtubedl_adServerURL') 
 469                         'title': info['title'], 
 470                         'entries': [ad_info, info], 
 475         if not info.get('url') and not info.get('formats'): 
 476             uploader_id = info.get('uploader_id') 
 478                 info.update(self._brightcove_new_url_result(uploader_id, video_id)) 
 480                 raise ExtractorError('Unable to extract video url for %s' % video_id) 
 484 class BrightcoveNewIE(AdobePassIE): 
 485     IE_NAME = 'brightcove:new' 
 486     _VALID_URL = r'https?://players\.brightcove\.net/(?P<account_id>\d+)/(?P<player_id>[^/]+)_(?P<embed>[^/]+)/index\.html\?.*videoId=(?P<video_id>\d+|ref:[^&]+)' 
 488         'url': 'http://players.brightcove.net/929656772001/e41d32dc-ec74-459e-a845-6c69f7b724ea_default/index.html?videoId=4463358922001', 
 489         'md5': 'c8100925723840d4b0d243f7025703be', 
 491             'id': '4463358922001', 
 493             'title': 'Meet the man behind Popcorn Time', 
 494             'description': 'md5:eac376a4fe366edc70279bfb681aea16', 
 496             'timestamp': 1441391203, 
 497             'upload_date': '20150904', 
 498             'uploader_id': '929656772001', 
 499             'formats': 'mincount:20', 
 503         'url': 'http://players.brightcove.net/4036320279001/5d112ed9-283f-485f-a7f9-33f42e8bc042_default/index.html?videoId=4279049078001', 
 505             'id': '4279049078001', 
 507             'title': 'Titansgrave: Chapter 0', 
 508             'description': 'Titansgrave: Chapter 0', 
 509             'duration': 1242.058, 
 510             'timestamp': 1433556729, 
 511             'upload_date': '20150606', 
 512             'uploader_id': '4036320279001', 
 513             'formats': 'mincount:39', 
 517             'skip_download': True, 
 520         # ref: prefixed video id 
 521         'url': 'http://players.brightcove.net/3910869709001/21519b5c-4b3b-4363-accb-bdc8f358f823_default/index.html?videoId=ref:7069442', 
 522         'only_matching': True, 
 524         # non numeric ref: prefixed video id 
 525         'url': 'http://players.brightcove.net/710858724001/default_default/index.html?videoId=ref:event-stream-356', 
 526         'only_matching': True, 
 528         # unavailable video without message but with error_code 
 529         'url': 'http://players.brightcove.net/1305187701/c832abfb-641b-44eb-9da0-2fe76786505f_default/index.html?videoId=4377407326001', 
 530         'only_matching': True, 
 534     def _extract_url(ie, webpage): 
 535         urls = BrightcoveNewIE._extract_urls(ie, webpage) 
 536         return urls[0] if urls else None 
 539     def _extract_urls(ie, webpage): 
 541         # 1. http://docs.brightcove.com/en/video-cloud/brightcove-player/guides/publish-video.html#setvideoiniframe 
 542         # 2. http://docs.brightcove.com/en/video-cloud/brightcove-player/guides/publish-video.html#tag 
 543         # 3. http://docs.brightcove.com/en/video-cloud/brightcove-player/guides/publish-video.html#setvideousingjavascript 
 544         # 4. http://docs.brightcove.com/en/video-cloud/brightcove-player/guides/in-page-embed-player-implementation.html 
 545         # 5. https://support.brightcove.com/en/video-cloud/docs/dynamically-assigning-videos-player 
 549         # Look for iframe embeds [1] 
 550         for _, url in re.findall( 
 551                 r'<iframe[^>]+src=(["\'])((?:https?:)?//players\.brightcove\.net/\d+/[^/]+/index\.html.+?)\1', webpage): 
 552             entries.append(url if url.startswith('http') else 'http:' + url) 
 554         # Look for <video> tags [2] and embed_in_page embeds [3] 
 556         for video, script_tag, account_id, player_id, embed in re.findall( 
 558                     (<video\s
+[^
>]*\bdata
-video
-id\s
*=\s
*['"]?[^>]+>) 
 561                             src=["\'](?:https?:)?//players\.brightcove\.net/ 
 562                             (\d+)/([^/]+)_([^/]+)/index(?:\.min)?\.js 
 566             attrs = extract_attributes(video) 
 568             # According to examples from [4] it's unclear whether video 
id 
 569             # may be optional and what to do when it is 
 570             video_id 
= attrs
.get('data-video-id') 
 574             account_id 
= account_id 
or attrs
.get('data-account') 
 578             player_id 
= player_id 
or attrs
.get('data-player') or 'default' 
 579             embed 
= embed 
or attrs
.get('data-embed') or 'default' 
 581             bc_url 
= 'http://players.brightcove.net/%s/%s_%s/index.html?videoId=%s' % ( 
 582                 account_id
, player_id
, embed
, video_id
) 
 584             # Some brightcove videos may be embedded with video tag only and 
 585             # without script tag or any mentioning of brightcove at all. Such 
 586             # embeds are considered ambiguous since they are matched based only 
 587             # on data-video-id and data-account attributes and in the wild may 
 588             # not be brightcove embeds at all. Let's check reconstructed 
 589             # brightcove URLs in case of such embeds and only process valid 
 590             # ones. By this we ensure there is indeed a brightcove embed. 
 591             if not script_tag 
and not ie
._is
_valid
_url
( 
 592                     bc_url
, video_id
, 'possible brightcove video'): 
 595             entries
.append(bc_url
) 
 599     def _parse_brightcove_metadata(self
, json_data
, video_id
, headers
={}): 
 600         title 
= json_data
['name'].strip() 
 603         for source 
in json_data
.get('sources', []): 
 604             container 
= source
.get('container') 
 605             ext 
= mimetype2ext(source
.get('type')) 
 606             src 
= source
.get('src') 
 607             # https://support.brightcove.com/playback-api-video-fields-reference#key_systems_object 
 608             if ext 
== 'ism' or container 
== 'WVM' or source
.get('key_systems'): 
 610             elif ext 
== 'm3u8' or container 
== 'M2TS': 
 613                 formats
.extend(self
._extract
_m
3u8_formats
( 
 614                     src
, video_id
, 'mp4', 'm3u8_native', m3u8_id
='hls', fatal
=False)) 
 618                 formats
.extend(self
._extract
_mpd
_formats
(src
, video_id
, 'dash', fatal
=False)) 
 620                 streaming_src 
= source
.get('streaming_src') 
 621                 stream_name
, app_name 
= source
.get('stream_name'), source
.get('app_name') 
 622                 if not src 
and not streaming_src 
and (not stream_name 
or not app_name
): 
 624                 tbr 
= float_or_none(source
.get('avg_bitrate'), 1000) 
 625                 height 
= int_or_none(source
.get('height')) 
 626                 width 
= int_or_none(source
.get('width')) 
 629                     'filesize': int_or_none(source
.get('size')), 
 630                     'container': container
, 
 631                     'ext': ext 
or container
.lower(), 
 633                 if width 
== 0 and height 
== 0: 
 641                         'vcodec': source
.get('codec'), 
 644                 def build_format_id(kind
): 
 647                         format_id 
+= '-%dk' % int(tbr
) 
 649                         format_id 
+= '-%dp' % height
 
 652                 if src 
or streaming_src
: 
 654                         'url': src 
or streaming_src
, 
 655                         'format_id': build_format_id('http' if src 
else 'http-streaming'), 
 656                         'source_preference': 0 if src 
else -1, 
 661                         'play_path': stream_name
, 
 662                         'format_id': build_format_id('rtmp'), 
 666             # for sonyliv.com DRM protected videos 
 667             s3_source_url 
= json_data
.get('custom_fields', {}).get('s3sourceurl') 
 670                     'url': s3_source_url
, 
 671                     'format_id': 'source', 
 674         errors 
= json_data
.get('errors') 
 675         if not formats 
and errors
: 
 677             raise ExtractorError( 
 678                 error
.get('message') or error
.get('error_subcode') or error
['error_code'], expected
=True) 
 680         self
._sort
_formats
(formats
) 
 683             f
.setdefault('http_headers', {}).update(headers
) 
 686         for text_track 
in json_data
.get('text_tracks', []): 
 687             if text_track
.get('src'): 
 688                 subtitles
.setdefault(text_track
.get('srclang'), []).append({ 
 689                     'url': text_track
['src'], 
 693         duration 
= float_or_none(json_data
.get('duration'), 1000) 
 694         if duration 
is not None and duration 
<= 0: 
 699             'title': self
._live
_title
(title
) if is_live 
else title
, 
 700             'description': clean_html(json_data
.get('description')), 
 701             'thumbnail': json_data
.get('thumbnail') or json_data
.get('poster'), 
 702             'duration': duration
, 
 703             'timestamp': parse_iso8601(json_data
.get('published_at')), 
 704             'uploader_id': json_data
.get('account_id'), 
 706             'subtitles': subtitles
, 
 707             'tags': json_data
.get('tags', []), 
 711     def _real_extract(self
, url
): 
 712         url
, smuggled_data 
= unsmuggle_url(url
, {}) 
 713         self
._initialize
_geo
_bypass
({ 
 714             'countries': smuggled_data
.get('geo_countries'), 
 715             'ip_blocks': smuggled_data
.get('geo_ip_blocks'), 
 718         account_id
, player_id
, embed
, video_id 
= re
.match(self
._VALID
_URL
, url
).groups() 
 720         webpage 
= self
._download
_webpage
( 
 721             'http://players.brightcove.net/%s/%s_%s/index.min.js' 
 722             % (account_id
, player_id
, embed
), video_id
) 
 726         catalog 
= self
._search
_regex
( 
 727             r
'catalog\(({.+?})\);', webpage
, 'catalog', default
=None) 
 729             catalog 
= self
._parse
_json
( 
 730                 js_to_json(catalog
), video_id
, fatal
=False) 
 732                 policy_key 
= catalog
.get('policyKey') 
 735             policy_key 
= self
._search
_regex
( 
 736                 r
'policyKey\s*:\s*(["\'])(?P
<pk
>.+?
)\
1', 
 737                 webpage, 'policy key
', group='pk
') 
 739         api_url = 'https
://edge
.api
.brightcove
.com
/playback
/v1
/accounts
/%s/videos
/%s' % (account_id, video_id) 
 741             'Accept
': 'application
/json
;pk
=%s' % policy_key, 
 743         referrer = smuggled_data.get('referrer
') 
 747                 'Origin
': re.search(r'https?
://[^
/]+', referrer).group(0), 
 750             json_data = self._download_json(api_url, video_id, headers=headers) 
 751         except ExtractorError as e: 
 752             if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403: 
 753                 json_data = self._parse_json(e.cause.read().decode(), video_id)[0] 
 754                 message = json_data.get('message
') or json_data['error_code
'] 
 755                 if json_data.get('error_subcode
') == 'CLIENT_GEO
': 
 756                     self.raise_geo_restricted(msg=message) 
 757                 raise ExtractorError(message, expected=True) 
 760         errors = json_data.get('errors
') 
 761         if errors and errors[0].get('error_subcode
') == 'TVE_AUTH
': 
 762             custom_fields = json_data['custom_fields
'] 
 763             tve_token = self._extract_mvpd_auth( 
 764                 smuggled_data['source_url
'], video_id, 
 765                 custom_fields['bcadobepassrequestorid
'], 
 766                 custom_fields['bcadobepassresourceid
']) 
 767             json_data = self._download_json( 
 768                 api_url, video_id, headers={ 
 769                     'Accept
': 'application
/json
;pk
=%s' % policy_key 
 771                     'tveToken
': tve_token, 
 774         return self._parse_brightcove_metadata( 
 775             json_data, video_id, headers=headers)