mirror of
https://codeberg.org/polarisfm/youtube-dl
synced 2024-11-23 00:54:31 +01:00
Add support for extracting subtitles from MPD manifests
This commit is contained in:
parent
049c0486bb
commit
fbfd54d0b8
@ -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,7 +829,8 @@ 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',
|
||||
@ -912,7 +913,8 @@ 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
|
||||
@ -958,18 +960,116 @@ 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(
|
||||
formats, subtitles = 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)
|
||||
expect_value(self, subtitles, expected_subtitles, None)
|
||||
|
||||
def test_parse_f4m_formats(self):
|
||||
_TEST_CASES = [
|
||||
|
31
test/testdata/mpd/subtitles.mpd
vendored
Normal file
31
test/testdata/mpd/subtitles.mpd
vendored
Normal 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>
|
@ -2023,7 +2023,10 @@ 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={}):
|
||||
def _extract_mpd_formats(self, *args, **kwargs):
|
||||
return self._extract_mpd_formats_subtitles(*args, **kwargs)[0]
|
||||
|
||||
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',
|
||||
@ -2036,13 +2039,16 @@ class InfoExtractor(object):
|
||||
return []
|
||||
mpd_base_url = base_url(urlh.geturl())
|
||||
|
||||
return self._parse_mpd_formats(
|
||||
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):
|
||||
def _parse_mpd_formats(self, *args, **kwargs):
|
||||
return self._parse_mpd_formats_subtitles(*args, **kwargs)[0]
|
||||
|
||||
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
|
||||
@ -2119,6 +2125,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, {
|
||||
@ -2134,26 +2141,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'))
|
||||
@ -2329,7 +2337,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, 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(
|
||||
|
@ -2390,11 +2390,14 @@ 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(
|
||||
formats, subtitles = self._parse_mpd_formats_subtitles(
|
||||
doc,
|
||||
mpd_base_url=full_response.geturl().rpartition('/')[0],
|
||||
mpd_url=url)
|
||||
self._sort_formats(info_dict['formats'])
|
||||
self._sort_formats(formats)
|
||||
info_dict['formats'] = formats
|
||||
if subtitles:
|
||||
info_dict['subtitles'] = subtitles
|
||||
return info_dict
|
||||
elif re.match(r'^{http://ns\.adobe\.com/f4m/[12]\.0}manifest$', doc.tag):
|
||||
info_dict['formats'] = self._parse_f4m_formats(doc, url, video_id)
|
||||
|
Loading…
Reference in New Issue
Block a user