This commit is contained in:
Lukas Anzinger 2020-09-25 15:17:06 +02:00 committed by GitHub
commit 33e0c88cef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 181 additions and 36 deletions

View File

@ -745,7 +745,7 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
self.ie._sort_formats(formats)
expect_value(self, formats, expected_formats, None)
def test_parse_mpd_formats(self):
def test_parse_mpd_formats_subtitles(self):
_TEST_CASES = [
(
# https://github.com/ytdl-org/youtube-dl/issues/13919
@ -829,8 +829,10 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
'tbr': 5997.485,
'width': 1920,
'height': 1080,
}]
), (
}],
{},
),
(
# https://github.com/ytdl-org/youtube-dl/pull/14844
'urls_only',
'http://unknown/manifest.mpd', # mpd_url
@ -912,8 +914,10 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
'tbr': 4400,
'width': 1920,
'height': 1080,
}]
), (
}],
{},
),
(
# https://github.com/ytdl-org/youtube-dl/issues/20346
# Media considered unfragmented even though it contains
# Initialization tag
@ -931,7 +935,6 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
'vcodec': 'none',
'tbr': 129.87,
'asr': 48000,
}, {
'url': 'https://v.redd.it/hw1x7rcg7zl21/DASH_240',
'manifest_url': 'https://v.redd.it/hw1x7rcg7zl21/DASHPlaylist.mpd',
@ -958,18 +961,117 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
'width': 360,
'height': 360,
'fps': 30,
}]
}],
{},
),
(
'subtitles',
'https://example.com/streams/1/playlist/playlist.mpd', # mpd_url
'https://example.com/streams/1/playlist', # mpd_base_url
[{'acodec': 'mp4a.40.2',
'asr': 48000,
'container': 'm4a_dash',
'ext': 'm4a',
'filesize': None,
'format_id': '131kbps',
'format_note': 'DASH audio',
'fps': None,
'fragment_base_url': 'https://example.com/streams/1/playlist/',
'fragments': [{'path': '../audio/1_stereo_131072/dash/init.mp4'},
{'duration': 3989.0,
'path': '../audio/1_stereo_131072/dash/segment_0.m4s'},
{'duration': 3989.0,
'path': '../audio/1_stereo_131072/dash/segment_1.m4s'}],
'height': None,
'language': 'de',
'manifest_url': 'https://example.com/streams/1/playlist/playlist.mpd',
'protocol': 'http_dash_segments',
'tbr': 131.072,
'url': 'https://example.com/streams/1/playlist/playlist.mpd',
'vcodec': 'none',
'width': None},
{'acodec': 'mp4a.40.2',
'asr': 48000,
'container': 'm4a_dash',
'ext': 'm4a',
'filesize': None,
'format_id': '196kbps',
'format_note': 'DASH audio',
'fps': None,
'fragment_base_url': 'https://example.com/streams/1/playlist/',
'fragments': [{'path': '../audio/1_stereo_196608/dash/init.mp4'},
{'duration': 3989.0,
'path': '../audio/1_stereo_196608/dash/segment_0.m4s'},
{'duration': 3989.0,
'path': '../audio/1_stereo_196608/dash/segment_1.m4s'}],
'height': None,
'language': 'de',
'manifest_url': 'https://example.com/streams/1/playlist/playlist.mpd',
'protocol': 'http_dash_segments',
'tbr': 196.608,
'url': 'https://example.com/streams/1/playlist/playlist.mpd',
'vcodec': 'none',
'width': None},
{'acodec': 'none',
'asr': None,
'container': 'mp4_dash',
'ext': 'mp4',
'filesize': None,
'format_id': '720p 1712kbps',
'format_note': 'DASH video',
'fps': 25,
'fragment_base_url': 'https://example.com/streams/1/playlist/',
'fragments': [{'path': '../video/720_1712128/dash/init.mp4'},
{'duration': 4000.0,
'path': '../video/720_1712128/dash/segment_0.m4s'},
{'duration': 4000.0,
'path': '../video/720_1712128/dash/segment_1.m4s'}],
'height': 720,
'language': None,
'manifest_url': 'https://example.com/streams/1/playlist/playlist.mpd',
'protocol': 'http_dash_segments',
'tbr': 1712.128,
'url': 'https://example.com/streams/1/playlist/playlist.mpd',
'vcodec': 'avc1.42c00d',
'width': 1280},
{'acodec': 'none',
'asr': None,
'container': 'mp4_dash',
'ext': 'mp4',
'filesize': None,
'format_id': '1080p 4669kbps',
'format_note': 'DASH video',
'fps': 25,
'fragment_base_url': 'https://example.com/streams/1/playlist/',
'fragments': [{'path': '../video/1080_4669440/dash/init.mp4'},
{'duration': 4000.0,
'path': '../video/1080_4669440/dash/segment_0.m4s'},
{'duration': 4000.0,
'path': '../video/1080_4669440/dash/segment_1.m4s'}],
'height': 1080,
'language': None,
'manifest_url': 'https://example.com/streams/1/playlist/playlist.mpd',
'protocol': 'http_dash_segments',
'tbr': 4669.44,
'url': 'https://example.com/streams/1/playlist/playlist.mpd',
'vcodec': 'avc1.42c00d',
'width': 1920}],
{'en': [{'ext': 'vtt',
'url': 'https://example.com/streams/1/subtitles/sub_en.vtt'}],
'fr': [{'ext': 'vtt',
'url': 'https://example.com/streams/1/subtitles/sub_fr.vtt'}]},
)
]
for mpd_file, mpd_url, mpd_base_url, expected_formats in _TEST_CASES:
for mpd_file, mpd_url, mpd_base_url, expected_formats, expected_subtitles in _TEST_CASES:
with io.open('./test/testdata/mpd/%s.mpd' % mpd_file,
mode='r', encoding='utf-8') as f:
formats = self.ie._parse_mpd_formats(
info_dict = self.ie._parse_mpd_formats_subtitles(
compat_etree_fromstring(f.read().encode('utf-8')),
mpd_base_url=mpd_base_url, mpd_url=mpd_url)
self.ie._sort_formats(formats)
expect_value(self, formats, expected_formats, None)
self.ie._sort_formats(info_dict['formats'])
expect_value(self, info_dict['formats'], expected_formats, None)
expect_value(self, info_dict['subtitles'], expected_subtitles, None)
def test_parse_f4m_formats(self):
_TEST_CASES = [

31
test/testdata/mpd/subtitles.mpd vendored Normal file
View File

@ -0,0 +1,31 @@
<?xml version='1.0' encoding='UTF-8'?>
<MPD availabilityStartTime="2015-11-10T10:01:31Z" id="e66e8502-59d8-42d4-9fc7-1ca87dbed590" mediaPresentationDuration="PT1H43M14S" minBufferTime="PT1S" profiles="urn:mpeg:dash:profile:isoff-main:2011" timeShiftBufferDepth="PT30S" type="static">
<Period>
<AdaptationSet codecs="avc1.42c00d" mimeType="video/mp4">
<Representation bandwidth="1712128" frameRate="25" height="720" id="720p 1712kbps" width="1280">
<SegmentTemplate duration="100000" initialization="../video/720_1712128/dash/init.mp4" media="../video/720_1712128/dash/segment_$Number$.m4s" startNumber="0" timescale="25" />
</Representation>
<Representation bandwidth="4669440" frameRate="25" height="1080" id="1080p 4669kbps" width="1920">
<SegmentTemplate duration="100000" initialization="../video/1080_4669440/dash/init.mp4" media="../video/1080_4669440/dash/segment_$Number$.m4s" startNumber="0" timescale="25" />
</Representation>
</AdaptationSet>
<AdaptationSet codecs="mp4a.40.2" lang="de" mimeType="audio/mp4" streamName="Deutsch stereo">
<Representation audioSamplingRate="48000" bandwidth="131072" id="131kbps">
<SegmentTemplate duration="191472" initialization="../audio/1_stereo_131072/dash/init.mp4" media="../audio/1_stereo_131072/dash/segment_$Number$.m4s" startNumber="0" timescale="48" />
</Representation>
<Representation audioSamplingRate="48000" bandwidth="196608" id="196kbps">
<SegmentTemplate duration="191472" initialization="../audio/1_stereo_196608/dash/init.mp4" media="../audio/1_stereo_196608/dash/segment_$Number$.m4s" startNumber="0" timescale="48" />
</Representation>
</AdaptationSet>
<AdaptationSet lang="fr" mimeType="text/vtt" streamName="Französisch">
<Representation>
<BaseURL>../subtitles/sub_fr.vtt</BaseURL>
</Representation>
</AdaptationSet>
<AdaptationSet lang="en" mimeType="text/vtt" streamName="Englisch">
<Representation>
<BaseURL>../subtitles/sub_en.vtt</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View File

@ -2056,6 +2056,11 @@ class InfoExtractor(object):
return entries
def _extract_mpd_formats(self, mpd_url, video_id, mpd_id=None, note=None, errnote=None, fatal=True, formats_dict={}, data=None, headers={}, query={}):
return self._extract_mpd_formats_subtitles(
mpd_url, video_id, mpd_id=mpd_id, note=note, errnote=errnote, fatal=fatal,
formats_dict=formats_dict, data=data, headers=headers, query=query)['formats']
def _extract_mpd_formats_subtitles(self, mpd_url, video_id, mpd_id=None, note=None, errnote=None, fatal=True, formats_dict={}, data=None, headers={}, query={}):
res = self._download_xml_handle(
mpd_url, video_id,
note=note or 'Downloading MPD manifest',
@ -2068,20 +2073,25 @@ class InfoExtractor(object):
return []
mpd_base_url = base_url(urlh.geturl())
return self._parse_mpd_formats(
mpd_doc, mpd_id=mpd_id, mpd_base_url=mpd_base_url,
formats_dict=formats_dict, mpd_url=mpd_url)
return self._parse_mpd_formats_subtitles(
mpd_doc, mpd_id=mpd_id, mpd_base_url=mpd_base_url, formats_dict=formats_dict,
mpd_url=mpd_url)
def _parse_mpd_formats(self, mpd_doc, mpd_id=None, mpd_base_url='', formats_dict={}, mpd_url=None):
return self._parse_mpd_formats_subtitles(
mpd_doc, mpd_id=mpd_id, mpd_base_url=mpd_base_url, formats_dict=formats_dict,
mpd_url=mpd_url)['formats']
def _parse_mpd_formats_subtitles(self, mpd_doc, mpd_id=None, mpd_base_url='', formats_dict={}, mpd_url=None):
"""
Parse formats from MPD manifest.
Parse formats and subtitles from MPD manifest.
References:
1. MPEG-DASH Standard, ISO/IEC 23009-1:2014(E),
http://standards.iso.org/ittf/PubliclyAvailableStandards/c065274_ISO_IEC_23009-1_2014.zip
2. https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP
"""
if mpd_doc.get('type') == 'dynamic':
return []
return {'formats': [], 'subtitles': {}}
namespace = self._search_regex(r'(?i)^{([^}]+)?}MPD$', mpd_doc.tag, 'namespace', default=None)
@ -2151,6 +2161,7 @@ class InfoExtractor(object):
mpd_duration = parse_duration(mpd_doc.get('mediaPresentationDuration'))
formats = []
subtitles = {}
for period in mpd_doc.findall(_add_ns('Period')):
period_duration = parse_duration(period.get('duration')) or mpd_duration
period_ms_info = extract_multisegment_info(period, {
@ -2166,26 +2177,27 @@ class InfoExtractor(object):
continue
representation_attrib = adaptation_set.attrib.copy()
representation_attrib.update(representation.attrib)
base_url = ''
for element in (representation, adaptation_set, period, mpd_doc):
base_url_e = element.find(_add_ns('BaseURL'))
if base_url_e is not None:
base_url = compat_urlparse.urljoin(base_url_e.text, base_url)
if re.match(r'^https?://', base_url):
break
if mpd_base_url and not re.match(r'^https?://', base_url):
if not mpd_base_url.endswith('/') and not base_url.startswith('/'):
mpd_base_url += '/'
base_url = compat_urlparse.urljoin(mpd_base_url, base_url)
representation_id = representation_attrib.get('id')
lang = representation_attrib.get('lang')
# According to [1, 5.3.7.2, Table 9, page 41], @mimeType is mandatory
mime_type = representation_attrib['mimeType']
content_type = mime_type.split('/')[0]
if content_type == 'text':
# TODO implement WebVTT downloading
pass
subtitles[lang] = [{'ext': mimetype2ext(mime_type), 'url': base_url}]
elif content_type in ('video', 'audio'):
base_url = ''
for element in (representation, adaptation_set, period, mpd_doc):
base_url_e = element.find(_add_ns('BaseURL'))
if base_url_e is not None:
base_url = base_url_e.text + base_url
if re.match(r'^https?://', base_url):
break
if mpd_base_url and not re.match(r'^https?://', base_url):
if not mpd_base_url.endswith('/') and not base_url.startswith('/'):
mpd_base_url += '/'
base_url = mpd_base_url + base_url
representation_id = representation_attrib.get('id')
lang = representation_attrib.get('lang')
url_el = representation.find(_add_ns('BaseURL'))
filesize = int_or_none(url_el.attrib.get('{http://youtube.com/yt/2012/10/10}contentLength') if url_el is not None else None)
bandwidth = int_or_none(representation_attrib.get('bandwidth'))
@ -2361,7 +2373,7 @@ class InfoExtractor(object):
formats.append(full_info)
else:
self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type)
return formats
return {'formats': formats, 'subtitles': subtitles}
def _extract_ism_formats(self, ism_url, video_id, ism_id=None, note=None, errnote=None, fatal=True, data=None, headers={}, query={}):
res = self._download_xml_handle(

View File

@ -2402,10 +2402,10 @@ class GenericIE(InfoExtractor):
xspf_base_url=full_response.geturl()),
video_id)
elif re.match(r'(?i)^(?:{[^}]+})?MPD$', doc.tag):
info_dict['formats'] = self._parse_mpd_formats(
doc,
mpd_base_url=full_response.geturl().rpartition('/')[0],
mpd_url=url)
info_dict.update(
self._parse_mpd_formats_subtitles(
doc, mpd_base_url=full_response.geturl().rpartition('/')[0],
mpd_url=url))
self._sort_formats(info_dict['formats'])
return info_dict
elif re.match(r'^{http://ns\.adobe\.com/f4m/[12]\.0}manifest$', doc.tag):