mirror of
https://codeberg.org/polarisfm/youtube-dl
synced 2024-11-26 10:24:33 +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)
|
self.ie._sort_formats(formats)
|
||||||
expect_value(self, formats, expected_formats, None)
|
expect_value(self, formats, expected_formats, None)
|
||||||
|
|
||||||
def test_parse_mpd_formats(self):
|
def test_parse_mpd_formats_subtitles(self):
|
||||||
_TEST_CASES = [
|
_TEST_CASES = [
|
||||||
(
|
(
|
||||||
# https://github.com/ytdl-org/youtube-dl/issues/13919
|
# 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,
|
'tbr': 5997.485,
|
||||||
'width': 1920,
|
'width': 1920,
|
||||||
'height': 1080,
|
'height': 1080,
|
||||||
}]
|
}],
|
||||||
|
{},
|
||||||
), (
|
), (
|
||||||
# https://github.com/ytdl-org/youtube-dl/pull/14844
|
# https://github.com/ytdl-org/youtube-dl/pull/14844
|
||||||
'urls_only',
|
'urls_only',
|
||||||
@ -912,7 +913,8 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
|
|||||||
'tbr': 4400,
|
'tbr': 4400,
|
||||||
'width': 1920,
|
'width': 1920,
|
||||||
'height': 1080,
|
'height': 1080,
|
||||||
}]
|
}],
|
||||||
|
{},
|
||||||
), (
|
), (
|
||||||
# https://github.com/ytdl-org/youtube-dl/issues/20346
|
# https://github.com/ytdl-org/youtube-dl/issues/20346
|
||||||
# Media considered unfragmented even though it contains
|
# Media considered unfragmented even though it contains
|
||||||
@ -958,18 +960,116 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
|
|||||||
'width': 360,
|
'width': 360,
|
||||||
'height': 360,
|
'height': 360,
|
||||||
'fps': 30,
|
'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,
|
with io.open('./test/testdata/mpd/%s.mpd' % mpd_file,
|
||||||
mode='r', encoding='utf-8') as f:
|
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')),
|
compat_etree_fromstring(f.read().encode('utf-8')),
|
||||||
mpd_base_url=mpd_base_url, mpd_url=mpd_url)
|
mpd_base_url=mpd_base_url, mpd_url=mpd_url)
|
||||||
self.ie._sort_formats(formats)
|
self.ie._sort_formats(formats)
|
||||||
expect_value(self, formats, expected_formats, None)
|
expect_value(self, formats, expected_formats, None)
|
||||||
|
expect_value(self, subtitles, expected_subtitles, None)
|
||||||
|
|
||||||
def test_parse_f4m_formats(self):
|
def test_parse_f4m_formats(self):
|
||||||
_TEST_CASES = [
|
_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
|
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(
|
res = self._download_xml_handle(
|
||||||
mpd_url, video_id,
|
mpd_url, video_id,
|
||||||
note=note or 'Downloading MPD manifest',
|
note=note or 'Downloading MPD manifest',
|
||||||
@ -2036,13 +2039,16 @@ class InfoExtractor(object):
|
|||||||
return []
|
return []
|
||||||
mpd_base_url = base_url(urlh.geturl())
|
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,
|
mpd_doc, mpd_id=mpd_id, mpd_base_url=mpd_base_url,
|
||||||
formats_dict=formats_dict, mpd_url=mpd_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:
|
References:
|
||||||
1. MPEG-DASH Standard, ISO/IEC 23009-1:2014(E),
|
1. MPEG-DASH Standard, ISO/IEC 23009-1:2014(E),
|
||||||
http://standards.iso.org/ittf/PubliclyAvailableStandards/c065274_ISO_IEC_23009-1_2014.zip
|
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'))
|
mpd_duration = parse_duration(mpd_doc.get('mediaPresentationDuration'))
|
||||||
formats = []
|
formats = []
|
||||||
|
subtitles = {}
|
||||||
for period in mpd_doc.findall(_add_ns('Period')):
|
for period in mpd_doc.findall(_add_ns('Period')):
|
||||||
period_duration = parse_duration(period.get('duration')) or mpd_duration
|
period_duration = parse_duration(period.get('duration')) or mpd_duration
|
||||||
period_ms_info = extract_multisegment_info(period, {
|
period_ms_info = extract_multisegment_info(period, {
|
||||||
@ -2134,26 +2141,27 @@ class InfoExtractor(object):
|
|||||||
continue
|
continue
|
||||||
representation_attrib = adaptation_set.attrib.copy()
|
representation_attrib = adaptation_set.attrib.copy()
|
||||||
representation_attrib.update(representation.attrib)
|
representation_attrib.update(representation.attrib)
|
||||||
# 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
|
|
||||||
elif content_type in ('video', 'audio'):
|
|
||||||
base_url = ''
|
base_url = ''
|
||||||
for element in (representation, adaptation_set, period, mpd_doc):
|
for element in (representation, adaptation_set, period, mpd_doc):
|
||||||
base_url_e = element.find(_add_ns('BaseURL'))
|
base_url_e = element.find(_add_ns('BaseURL'))
|
||||||
if base_url_e is not None:
|
if base_url_e is not None:
|
||||||
base_url = base_url_e.text + base_url
|
base_url = compat_urlparse.urljoin(base_url_e.text, base_url)
|
||||||
if re.match(r'^https?://', base_url):
|
if re.match(r'^https?://', base_url):
|
||||||
break
|
break
|
||||||
if mpd_base_url and not re.match(r'^https?://', base_url):
|
if mpd_base_url and not re.match(r'^https?://', base_url):
|
||||||
if not mpd_base_url.endswith('/') and not base_url.startswith('/'):
|
if not mpd_base_url.endswith('/') and not base_url.startswith('/'):
|
||||||
mpd_base_url += '/'
|
mpd_base_url += '/'
|
||||||
base_url = mpd_base_url + base_url
|
base_url = compat_urlparse.urljoin(mpd_base_url, base_url)
|
||||||
representation_id = representation_attrib.get('id')
|
representation_id = representation_attrib.get('id')
|
||||||
lang = representation_attrib.get('lang')
|
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':
|
||||||
|
subtitles[lang] = [{'ext': mimetype2ext(mime_type), 'url': base_url}]
|
||||||
|
elif content_type in ('video', 'audio'):
|
||||||
url_el = representation.find(_add_ns('BaseURL'))
|
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)
|
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'))
|
bandwidth = int_or_none(representation_attrib.get('bandwidth'))
|
||||||
@ -2329,7 +2337,7 @@ class InfoExtractor(object):
|
|||||||
formats.append(full_info)
|
formats.append(full_info)
|
||||||
else:
|
else:
|
||||||
self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type)
|
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={}):
|
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(
|
res = self._download_xml_handle(
|
||||||
|
@ -2390,11 +2390,14 @@ class GenericIE(InfoExtractor):
|
|||||||
xspf_base_url=full_response.geturl()),
|
xspf_base_url=full_response.geturl()),
|
||||||
video_id)
|
video_id)
|
||||||
elif re.match(r'(?i)^(?:{[^}]+})?MPD$', doc.tag):
|
elif re.match(r'(?i)^(?:{[^}]+})?MPD$', doc.tag):
|
||||||
info_dict['formats'] = self._parse_mpd_formats(
|
formats, subtitles = self._parse_mpd_formats_subtitles(
|
||||||
doc,
|
doc,
|
||||||
mpd_base_url=full_response.geturl().rpartition('/')[0],
|
mpd_base_url=full_response.geturl().rpartition('/')[0],
|
||||||
mpd_url=url)
|
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
|
return info_dict
|
||||||
elif re.match(r'^{http://ns\.adobe\.com/f4m/[12]\.0}manifest$', doc.tag):
|
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)
|
info_dict['formats'] = self._parse_f4m_formats(doc, url, video_id)
|
||||||
|
Loading…
Reference in New Issue
Block a user