mirror of https://github.com/streamlink/streamlink
stream.dash: fix segment availability times
The segment availability "anchor time" depends on the sum of the manifest's `availabilityStartTime` and the period's `start` attribute, for both static and dynamic manifests. Fix segment availability times: - Set the correct `available_at` value for static manifests with segment templates and segment timelines instead of using the current time - Set the `available_at` value for `SegmentList` segments instead of defaulting to `EPOCH_START` - Set the `available_at` value for all initialization segments Fix segment numbers: - The number offset now also takes the period start into consideration Also: - Allow passing keywords to child node constructors - Keep the `Period` reference on `SegmentList`, `SegmentTemplate` and `Representation` - Check segment availability times in certain tests - Rename DASH manifest fixture files of updated tests
This commit is contained in:
parent
1b9ce00454
commit
651739f236
|
@ -212,13 +212,14 @@ class MPDNode:
|
|||
cls: Type[TMPDNode],
|
||||
minimum: int = 0,
|
||||
maximum: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> List[TMPDNode]:
|
||||
children = self.node.findall(cls.__tag__)
|
||||
if len(children) < minimum or (maximum and len(children) > maximum):
|
||||
raise MPDParsingError(f"Expected to find {self.__tag__}/{cls.__tag__} required [{minimum}..{maximum or 'unbound'})")
|
||||
|
||||
return [
|
||||
cls(child, root=self.root, parent=self, i=i, base_url=self.base_url)
|
||||
cls(child, root=self.root, parent=self, i=i, base_url=self.base_url, **kwargs)
|
||||
for i, child in enumerate(children)
|
||||
]
|
||||
|
||||
|
@ -226,8 +227,9 @@ class MPDNode:
|
|||
self,
|
||||
cls: Type[TMPDNode],
|
||||
minimum: int = 0,
|
||||
**kwargs,
|
||||
) -> Optional[TMPDNode]:
|
||||
children = self.children(cls, minimum=minimum, maximum=1)
|
||||
children = self.children(cls, minimum=minimum, maximum=1, **kwargs)
|
||||
return children[0] if len(children) else None
|
||||
|
||||
def walk_back(
|
||||
|
@ -304,7 +306,7 @@ class MPD(MPDNode):
|
|||
self.availabilityStartTime = self.attr(
|
||||
"availabilityStartTime",
|
||||
parser=MPDParsers.datetime,
|
||||
default=datetime.datetime.fromtimestamp(0, UTC), # earliest date
|
||||
default=EPOCH_START,
|
||||
required=self.type == "dynamic",
|
||||
)
|
||||
self.publishTime = self.attr(
|
||||
|
@ -398,17 +400,18 @@ class Period(MPDNode):
|
|||
default=Duration(),
|
||||
)
|
||||
|
||||
if self.start is None and self.i == 0 and self.root.type == "static":
|
||||
self.start = 0
|
||||
# anchor time for segment availability
|
||||
offset = self.start if self.root.type == "dynamic" else Duration()
|
||||
self.availabilityStartTime = self.root.availabilityStartTime + offset
|
||||
|
||||
# TODO: Early Access Periods
|
||||
|
||||
self.baseURLs = self.children(BaseURL)
|
||||
self.segmentBase = self.only_child(SegmentBase)
|
||||
self.segmentBase = self.only_child(SegmentBase, period=self)
|
||||
self.segmentList = self.only_child(SegmentList, period=self)
|
||||
self.segmentTemplate = self.only_child(SegmentTemplate, period=self)
|
||||
self.adaptationSets = self.children(AdaptationSet, minimum=1)
|
||||
self.segmentList = self.only_child(SegmentList)
|
||||
self.segmentTemplate = self.only_child(SegmentTemplate)
|
||||
self.sssetIdentifier = self.only_child(AssetIdentifier)
|
||||
self.assetIdentifier = self.only_child(AssetIdentifier)
|
||||
self.eventStream = self.children(EventStream)
|
||||
self.subset = self.children(Subset)
|
||||
|
||||
|
@ -458,9 +461,13 @@ class SegmentURL(MPDNode):
|
|||
class SegmentList(MPDNode):
|
||||
__tag__ = "SegmentList"
|
||||
|
||||
def __init__(self, node, root=None, parent=None, *args, **kwargs):
|
||||
period: "Period"
|
||||
|
||||
def __init__(self, node, root=None, parent=None, period=None, *args, **kwargs):
|
||||
super().__init__(node, root, parent, *args, **kwargs)
|
||||
|
||||
self.period = period
|
||||
|
||||
self.presentation_time_offset = self.attr("presentationTimeOffset")
|
||||
self.timescale = self.attr(
|
||||
"timescale",
|
||||
|
@ -486,18 +493,20 @@ class SegmentList(MPDNode):
|
|||
|
||||
@property
|
||||
def segments(self) -> Iterator[Segment]:
|
||||
if self.initialization:
|
||||
if self.initialization: # pragma: no branch
|
||||
yield Segment(
|
||||
url=self.make_url(self.initialization.source_url),
|
||||
duration=0,
|
||||
init=True,
|
||||
content=False,
|
||||
available_at=self.period.availabilityStartTime,
|
||||
byterange=self.initialization.range,
|
||||
)
|
||||
for n, segment_url in enumerate(self.segment_urls, self.start_number):
|
||||
yield Segment(
|
||||
url=self.make_url(segment_url.media),
|
||||
duration=self.duration_seconds,
|
||||
available_at=self.period.availabilityStartTime,
|
||||
byterange=segment_url.media_range,
|
||||
)
|
||||
|
||||
|
@ -566,8 +575,8 @@ class AdaptationSet(MPDNode):
|
|||
)
|
||||
|
||||
self.baseURLs = self.children(BaseURL)
|
||||
self.segmentTemplate = self.only_child(SegmentTemplate)
|
||||
self.representations = self.children(Representation, minimum=1)
|
||||
self.segmentTemplate = self.only_child(SegmentTemplate, period=self.parent)
|
||||
self.representations = self.children(Representation, minimum=1, period=self.parent)
|
||||
self.contentProtection = self.children(ContentProtection)
|
||||
|
||||
|
||||
|
@ -575,10 +584,13 @@ class SegmentTemplate(MPDNode):
|
|||
__tag__ = "SegmentTemplate"
|
||||
|
||||
parent: Union["Period", "AdaptationSet", "Representation"]
|
||||
period: "Period"
|
||||
|
||||
def __init__(self, node, root=None, parent=None, *args, **kwargs):
|
||||
def __init__(self, node, root=None, parent=None, period=None, *args, **kwargs):
|
||||
super().__init__(node, root, parent, *args, **kwargs)
|
||||
|
||||
self.period = period
|
||||
|
||||
self.defaultSegmentTemplate = self.walk_back_get_attr("segmentTemplate")
|
||||
|
||||
self.initialization = self.attr(
|
||||
|
@ -614,20 +626,19 @@ class SegmentTemplate(MPDNode):
|
|||
else:
|
||||
self.duration_seconds = None
|
||||
|
||||
self.period = list(self.walk_back(Period))[0]
|
||||
|
||||
# children
|
||||
self.segmentTimeline = self.only_child(SegmentTimeline)
|
||||
|
||||
def segments(self, ident: TTimelineIdent, base_url: str, **kwargs) -> Iterator[Segment]:
|
||||
if kwargs.pop("init", True):
|
||||
if kwargs.pop("init", True): # pragma: no branch
|
||||
init_url = self.format_initialization(base_url, **kwargs)
|
||||
if init_url:
|
||||
if init_url: # pragma: no branch
|
||||
yield Segment(
|
||||
url=init_url,
|
||||
duration=0,
|
||||
init=True,
|
||||
content=False,
|
||||
available_at=self.period.availabilityStartTime,
|
||||
)
|
||||
for media_url, available_at in self.format_media(ident, base_url, **kwargs):
|
||||
yield Segment(
|
||||
|
@ -663,7 +674,7 @@ class SegmentTemplate(MPDNode):
|
|||
available_iter: Iterator[datetime.datetime]
|
||||
|
||||
if self.root.type == "static":
|
||||
available_iter = repeat(EPOCH_START)
|
||||
available_iter = repeat(self.period.availabilityStartTime)
|
||||
duration = self.period.duration.seconds or self.root.mediaPresentationDuration.seconds
|
||||
if duration:
|
||||
number_iter = range(self.startNumber, int(duration / self.duration_seconds) + 1)
|
||||
|
@ -672,11 +683,10 @@ class SegmentTemplate(MPDNode):
|
|||
else:
|
||||
now = datetime.datetime.now(UTC)
|
||||
if self.presentationTimeOffset:
|
||||
since_start = (now - self.presentationTimeOffset) - self.root.availabilityStartTime
|
||||
available_start_date = self.root.availabilityStartTime + self.presentationTimeOffset + since_start
|
||||
available_start = available_start_date
|
||||
since_start = (now - self.presentationTimeOffset) - self.period.availabilityStartTime
|
||||
available_start = self.period.availabilityStartTime + self.presentationTimeOffset + since_start
|
||||
else:
|
||||
since_start = now - self.root.availabilityStartTime
|
||||
since_start = now - self.period.availabilityStartTime
|
||||
available_start = now
|
||||
|
||||
# if there is no delay, use a delay of 3 seconds
|
||||
|
@ -709,9 +719,9 @@ class SegmentTemplate(MPDNode):
|
|||
log.debug(f"Generating segment timeline for {self.root.type} playlist: {ident!r}")
|
||||
|
||||
if self.root.type == "static":
|
||||
available_at = self.period.availabilityStartTime
|
||||
for segment, n in zip(self.segmentTimeline.segments, count(self.startNumber)):
|
||||
url = self.make_url(base_url, self.media(Time=segment.t, Number=n, **kwargs))
|
||||
available_at = datetime.datetime.now(tz=UTC) # TODO: replace with EPOCH_START ?!
|
||||
yield url, available_at
|
||||
return
|
||||
|
||||
|
@ -749,10 +759,13 @@ class Representation(MPDNode):
|
|||
__tag__ = "Representation"
|
||||
|
||||
parent: "AdaptationSet"
|
||||
period: "Period"
|
||||
|
||||
def __init__(self, node, root=None, parent=None, *args, **kwargs):
|
||||
def __init__(self, node, root=None, parent=None, period=None, *args, **kwargs):
|
||||
super().__init__(node, root, parent, *args, **kwargs)
|
||||
|
||||
self.period = period
|
||||
|
||||
self.id = self.attr(
|
||||
"id",
|
||||
required=True,
|
||||
|
@ -805,9 +818,9 @@ class Representation(MPDNode):
|
|||
|
||||
self.baseURLs = self.children(BaseURL)
|
||||
self.subRepresentation = self.children(SubRepresentation)
|
||||
self.segmentBase = self.only_child(SegmentBase)
|
||||
self.segmentList = self.children(SegmentList)
|
||||
self.segmentTemplate = self.only_child(SegmentTemplate)
|
||||
self.segmentBase = self.only_child(SegmentBase, period=self.period)
|
||||
self.segmentList = self.children(SegmentList, period=self.period)
|
||||
self.segmentTemplate = self.only_child(SegmentTemplate, period=self.period)
|
||||
self.contentProtection = self.children(ContentProtection)
|
||||
|
||||
@property
|
||||
|
@ -846,6 +859,7 @@ class Representation(MPDNode):
|
|||
duration=0,
|
||||
init=True,
|
||||
content=True,
|
||||
available_at=self.period.availabilityStartTime,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
|
||||
profiles="urn:mpeg:dash:profile:isoff-live:2011"
|
||||
type="static"
|
||||
availabilityStartTime="2020-01-01T00:00:00Z"
|
||||
mediaPresentationDuration="PT0H0M6.00S"
|
||||
minBufferTime="PT6.0S"
|
||||
>
|
||||
<BaseURL>https://hostname/</BaseURL>
|
||||
<Period id="0" start="PT0.0S">
|
||||
<Period id="0" start="PT12M34S">
|
||||
<BaseURL>period/</BaseURL>
|
||||
<AdaptationSet
|
||||
id="0"
|
||||
|
|
|
@ -5,13 +5,15 @@
|
|||
xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
|
||||
profiles="urn:mpeg:dash:profile:isoff-main:2011"
|
||||
type="static"
|
||||
availabilityStartTime="2020-01-01T00:00:00Z"
|
||||
publishTime="2018-06-25T18:01:58Z"
|
||||
mediaPresentationDuration="PT53M18.059S"
|
||||
minBufferTime="PT1.5S">
|
||||
minBufferTime="PT1.5S"
|
||||
>
|
||||
<ProgramInformation>
|
||||
<Title>test/dash.smil</Title>
|
||||
</ProgramInformation>
|
||||
<Period id="0" start="PT0.0S">
|
||||
<Period id="0" start="PT12M34S">
|
||||
<AdaptationSet id="0" group="1" mimeType="video/mp4" maxWidth="1920" maxHeight="1080" par="16:9" frameRate="25" segmentAlignment="true" startWithSAP="1" subsegmentAlignment="true" subsegmentStartsWithSAP="1">
|
||||
<Representation id="p0va0br4332748" codecs="avc1.640028" width="1920" height="1080" sar="1:1" bandwidth="4332748">
|
||||
<SegmentList presentationTimeOffset="0" timescale="90000" duration="900000" startNumber="1">
|
|
@ -1,7 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:cenc="urn:mpeg:cenc:2013" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd" type="dynamic" publishTime="2018-05-10T09:27:09" minimumUpdatePeriod="PT10S" availabilityStartTime="2018-05-04T13:20:07Z" minBufferTime="PT2S" suggestedPresentationDelay="PT40S" timeShiftBufferDepth="PT24H0M0S" profiles="urn:mpeg:dash:profile:isoff-live:2011">
|
||||
<Period start="PT0S" id="1">
|
||||
<AdaptationSet mimeType="video/mp4" frameRate="25/1" segmentAlignment="true" subsegmentAlignment="true" startWithSAP="1" subsegmentStartsWithSAP="1" bitstreamSwitching="false">
|
||||
<MPD
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:cenc="urn:mpeg:cenc:2013"
|
||||
xmlns="urn:mpeg:dash:schema:mpd:2011"
|
||||
xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
|
||||
type="dynamic"
|
||||
availabilityStartTime="2018-05-04T13:20:07Z"
|
||||
publishTime="2018-05-10T09:27:09"
|
||||
minimumUpdatePeriod="PT10S"
|
||||
minBufferTime="PT2S"
|
||||
suggestedPresentationDelay="PT40S"
|
||||
timeShiftBufferDepth="PT24H0M0S"
|
||||
profiles="urn:mpeg:dash:profile:isoff-live:2011"
|
||||
>
|
||||
<Period id="1" start="PT12M34S">
|
||||
<AdaptationSet
|
||||
mimeType="video/mp4"
|
||||
frameRate="25/1"
|
||||
segmentAlignment="true"
|
||||
subsegmentAlignment="true"
|
||||
startWithSAP="1"
|
||||
subsegmentStartsWithSAP="1"
|
||||
bitstreamSwitching="false"
|
||||
>
|
||||
<SegmentTemplate timescale="90000" duration="450000" startNumber="1"/>
|
||||
<Representation id="1" width="1280" height="720" bandwidth="2400000" codecs="avc1.64001f">
|
||||
<SegmentTemplate duration="450000" startNumber="1" media="hd-5_$Number%09d$.mp4" initialization="hd-5-init.mp4"/>
|
||||
|
@ -22,14 +43,33 @@
|
|||
<SegmentTemplate duration="450000" startNumber="1" media="hd-0_$Number%09d$.mp4" initialization="hd-0-init.mp4"/>
|
||||
</Representation>
|
||||
</AdaptationSet>
|
||||
<AdaptationSet mimeType="audio/mp4" lang="rus" segmentAlignment="0">
|
||||
<SegmentTemplate timescale="48000" media="hd-audio_$Number%09d$.mp4" initialization="hd-audio-init.mp4" duration="240000" startNumber="1"/>
|
||||
<AdaptationSet
|
||||
mimeType="audio/mp4"
|
||||
lang="rus"
|
||||
segmentAlignment="0"
|
||||
>
|
||||
<SegmentTemplate
|
||||
timescale="48000"
|
||||
media="hd-audio_$Number%09d$.mp4"
|
||||
initialization="hd-audio-init.mp4"
|
||||
duration="240000"
|
||||
startNumber="1"
|
||||
/>
|
||||
<Representation id="7" bandwidth="96000" audioSamplingRate="48000" codecs="mp4a.40.2"/>
|
||||
</AdaptationSet>
|
||||
<AdaptationSet mimeType="application/mp4" lang="rus">
|
||||
<AdaptationSet
|
||||
mimeType="application/mp4"
|
||||
lang="rus"
|
||||
>
|
||||
<Role schemeIdUri="urn:mpeg:dash:role" value="subtitle"/>
|
||||
<SegmentTemplate timescale="90000" media="hd-caption_$Number%09d$.mp4" initialization="hd-caption-init.mp4" duration="450000" startNumber="1"/>
|
||||
<SegmentTemplate
|
||||
timescale="90000"
|
||||
media="hd-caption_$Number%09d$.mp4"
|
||||
initialization="hd-caption-init.mp4"
|
||||
duration="450000"
|
||||
startNumber="1"
|
||||
/>
|
||||
<Representation id="8" bandwidth="256" codecs="stpp"/>
|
||||
</AdaptationSet>
|
||||
</Period>
|
||||
</MPD>
|
||||
</MPD>
|
|
@ -5,11 +5,13 @@
|
|||
xmlns="urn:mpeg:dash:schema:mpd:2011"
|
||||
xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
|
||||
type="static"
|
||||
availabilityStartTime="2020-01-01T00:00:00Z"
|
||||
mediaPresentationDuration="PT1M23.847347S"
|
||||
maxSegmentDuration="PT3S"
|
||||
minBufferTime="PT10S"
|
||||
profiles="urn:mpeg:dash:profile:isoff-live:2011">
|
||||
<Period>
|
||||
profiles="urn:mpeg:dash:profile:isoff-live:2011"
|
||||
>
|
||||
<Period start="PT12M34S">
|
||||
<BaseURL>dash/</BaseURL>
|
||||
<AdaptationSet
|
||||
group="1"
|
||||
|
@ -131,4 +133,4 @@
|
|||
</Representation>
|
||||
</AdaptationSet>
|
||||
</Period>
|
||||
</MPD>
|
||||
</MPD>
|
|
@ -6,7 +6,6 @@ from unittest.mock import Mock
|
|||
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
from freezegun.api import FakeDatetime # type: ignore[attr-defined]
|
||||
|
||||
from streamlink.stream.dash_manifest import MPD, MPDParsers, MPDParsingError, Representation
|
||||
from tests.resources import xml
|
||||
|
@ -110,64 +109,68 @@ class TestMPDParser(unittest.TestCase):
|
|||
]
|
||||
|
||||
def test_segments_dynamic_number(self):
|
||||
with freeze_time(FakeDatetime(2018, 5, 22, 13, 37, 0, tzinfo=UTC)):
|
||||
with xml("dash/test_4.mpd") as mpd_xml:
|
||||
mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd")
|
||||
|
||||
segments = mpd.periods[0].adaptationSets[0].representations[0].segments()
|
||||
init_segment = next(segments)
|
||||
assert init_segment.url == "http://test.se/hd-5-init.mp4"
|
||||
|
||||
video_segments = []
|
||||
for _ in range(3):
|
||||
seg = next(segments)
|
||||
video_segments.append((seg.url,
|
||||
seg.available_at))
|
||||
|
||||
assert video_segments == [
|
||||
(
|
||||
"http://test.se/hd-5_000311235.mp4",
|
||||
datetime.datetime(2018, 5, 22, 13, 37, 0, tzinfo=UTC),
|
||||
),
|
||||
(
|
||||
"http://test.se/hd-5_000311236.mp4",
|
||||
datetime.datetime(2018, 5, 22, 13, 37, 5, tzinfo=UTC),
|
||||
),
|
||||
(
|
||||
"http://test.se/hd-5_000311237.mp4",
|
||||
datetime.datetime(2018, 5, 22, 13, 37, 10, tzinfo=UTC),
|
||||
),
|
||||
]
|
||||
|
||||
def test_segments_static_no_publish_time(self):
|
||||
with xml("dash/test_5.mpd") as mpd_xml:
|
||||
mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd")
|
||||
|
||||
segments = mpd.periods[0].adaptationSets[1].representations[0].segments()
|
||||
init_segment = next(segments)
|
||||
assert init_segment.url == "http://test.se/dash/150633-video_eng=194000.dash"
|
||||
|
||||
video_segments = [x.url for x in itertools.islice(segments, 3)]
|
||||
assert video_segments == [
|
||||
"http://test.se/dash/150633-video_eng=194000-0.dash",
|
||||
"http://test.se/dash/150633-video_eng=194000-2000.dash",
|
||||
"http://test.se/dash/150633-video_eng=194000-4000.dash",
|
||||
with xml("dash/test_segments_dynamic_number.mpd") as mpd_xml, \
|
||||
freeze_time("2018-05-22T13:37:00Z"):
|
||||
mpd = MPD(mpd_xml, base_url="http://test/", url="http://test/manifest.mpd")
|
||||
stream_urls = [
|
||||
(segment.url, segment.available_at)
|
||||
for segment in itertools.islice(mpd.periods[0].adaptationSets[0].representations[0].segments(), 4)
|
||||
]
|
||||
|
||||
def test_segments_list(self):
|
||||
with xml("dash/test_7.mpd") as mpd_xml:
|
||||
mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd")
|
||||
assert stream_urls == [
|
||||
# The initialization segment gets its availability time from
|
||||
# the sum of the manifest's availabilityStartTime value and the period's start value, similar to static manifests
|
||||
(
|
||||
"http://test/hd-5-init.mp4",
|
||||
datetime.datetime(2018, 5, 4, 13, 32, 41, tzinfo=UTC),
|
||||
),
|
||||
# The segment number also takes the availabilityStartTime and period start sum into consideration,
|
||||
# but the availability time depends on the current time and the segment durations
|
||||
(
|
||||
"http://test/hd-5_000311084.mp4",
|
||||
datetime.datetime(2018, 5, 22, 13, 37, 0, tzinfo=UTC),
|
||||
),
|
||||
(
|
||||
"http://test/hd-5_000311085.mp4",
|
||||
datetime.datetime(2018, 5, 22, 13, 37, 5, tzinfo=UTC),
|
||||
),
|
||||
(
|
||||
"http://test/hd-5_000311086.mp4",
|
||||
datetime.datetime(2018, 5, 22, 13, 37, 10, tzinfo=UTC),
|
||||
),
|
||||
]
|
||||
|
||||
segments = mpd.periods[0].adaptationSets[0].representations[0].segments()
|
||||
init_segment = next(segments)
|
||||
assert init_segment.url == "http://test.se/chunk_ctvideo_ridp0va0br4332748_cinit_mpd.m4s"
|
||||
def test_static_no_publish_time(self):
|
||||
with xml("dash/test_static_no_publish_time.mpd") as mpd_xml:
|
||||
mpd = MPD(mpd_xml, base_url="http://test/", url="http://test/manifest.mpd")
|
||||
|
||||
video_segments = [x.url for x in itertools.islice(segments, 3)]
|
||||
assert video_segments == [
|
||||
"http://test.se/chunk_ctvideo_ridp0va0br4332748_cn1_mpd.m4s",
|
||||
"http://test.se/chunk_ctvideo_ridp0va0br4332748_cn2_mpd.m4s",
|
||||
"http://test.se/chunk_ctvideo_ridp0va0br4332748_cn3_mpd.m4s",
|
||||
]
|
||||
segments = mpd.periods[0].adaptationSets[1].representations[0].segments()
|
||||
segment_urls = [(segment.url, segment.available_at) for segment in itertools.islice(segments, 4)]
|
||||
# ignores period start time in static manifests
|
||||
expected_availability = datetime.datetime(2020, 1, 1, 0, 0, 0, tzinfo=UTC)
|
||||
|
||||
assert segment_urls == [
|
||||
("http://test/dash/150633-video_eng=194000.dash", expected_availability),
|
||||
("http://test/dash/150633-video_eng=194000-0.dash", expected_availability),
|
||||
("http://test/dash/150633-video_eng=194000-2000.dash", expected_availability),
|
||||
("http://test/dash/150633-video_eng=194000-4000.dash", expected_availability),
|
||||
]
|
||||
|
||||
def test_segment_list(self):
|
||||
with xml("dash/test_segment_list.mpd") as mpd_xml:
|
||||
mpd = MPD(mpd_xml, base_url="http://test/", url="http://test/manifest.mpd")
|
||||
|
||||
segments = mpd.periods[0].adaptationSets[0].representations[0].segments()
|
||||
segment_urls = [(segment.url, segment.available_at) for segment in itertools.islice(segments, 4)]
|
||||
# ignores period start time in static manifests
|
||||
expected_availability = datetime.datetime(2020, 1, 1, 0, 0, 0, tzinfo=UTC)
|
||||
|
||||
assert segment_urls == [
|
||||
("http://test/chunk_ctvideo_ridp0va0br4332748_cinit_mpd.m4s", expected_availability),
|
||||
("http://test/chunk_ctvideo_ridp0va0br4332748_cn1_mpd.m4s", expected_availability),
|
||||
("http://test/chunk_ctvideo_ridp0va0br4332748_cn2_mpd.m4s", expected_availability),
|
||||
("http://test/chunk_ctvideo_ridp0va0br4332748_cn3_mpd.m4s", expected_availability),
|
||||
]
|
||||
|
||||
def test_segments_dynamic_timeline_continue(self):
|
||||
with xml("dash/test_6_p1.mpd") as mpd_xml_p1:
|
||||
|
@ -265,13 +268,16 @@ class TestMPDParser(unittest.TestCase):
|
|||
def test_segments_byterange(self):
|
||||
with xml("dash/test_segments_byterange.mpd") as mpd_xml:
|
||||
mpd = MPD(mpd_xml, base_url="http://test/", url="http://test/manifest.mpd")
|
||||
assert [
|
||||
|
||||
segment_urls = [
|
||||
[
|
||||
(seg.url, seg.init, seg.byterange)
|
||||
for seg in adaptationset.representations[0].segments()
|
||||
]
|
||||
for adaptationset in mpd.periods[0].adaptationSets
|
||||
] == [
|
||||
]
|
||||
|
||||
assert segment_urls == [
|
||||
[
|
||||
("http://test/video-frag.mp4", True, (36, 711)),
|
||||
("http://test/video-frag.mp4", False, (747, 875371)),
|
||||
|
@ -291,30 +297,34 @@ class TestMPDParser(unittest.TestCase):
|
|||
def test_nested_baseurls(self):
|
||||
with xml("dash/test_nested_baseurls.mpd") as mpd_xml:
|
||||
mpd = MPD(mpd_xml, base_url="https://foo/", url="https://test/manifest.mpd")
|
||||
|
||||
segment_urls = [
|
||||
[seg.url for seg in itertools.islice(representation.segments(), 2)]
|
||||
[(segment.url, segment.available_at) for segment in itertools.islice(representation.segments(), 2)]
|
||||
for adaptationset in mpd.periods[0].adaptationSets for representation in adaptationset.representations
|
||||
]
|
||||
# ignores period start time in static manifests
|
||||
expected_availability = datetime.datetime(2020, 1, 1, 0, 0, 0, tzinfo=UTC)
|
||||
|
||||
assert segment_urls == [
|
||||
[
|
||||
"https://hostname/period/init_video_5000kbps.m4s",
|
||||
"https://hostname/period/media_video_5000kbps-1.m4s",
|
||||
("https://hostname/period/init_video_5000kbps.m4s", expected_availability),
|
||||
("https://hostname/period/media_video_5000kbps-1.m4s", expected_availability),
|
||||
],
|
||||
[
|
||||
"https://hostname/period/representation/init_video_9000kbps.m4s",
|
||||
"https://hostname/period/representation/media_video_9000kbps-1.m4s",
|
||||
("https://hostname/period/representation/init_video_9000kbps.m4s", expected_availability),
|
||||
("https://hostname/period/representation/media_video_9000kbps-1.m4s", expected_availability),
|
||||
],
|
||||
[
|
||||
"https://hostname/period/adaptationset/init_audio_128kbps.m4s",
|
||||
"https://hostname/period/adaptationset/media_audio_128kbps-1.m4s",
|
||||
("https://hostname/period/adaptationset/init_audio_128kbps.m4s", expected_availability),
|
||||
("https://hostname/period/adaptationset/media_audio_128kbps-1.m4s", expected_availability),
|
||||
],
|
||||
[
|
||||
"https://hostname/period/adaptationset/representation/init_audio_256kbps.m4s",
|
||||
"https://hostname/period/adaptationset/representation/media_audio_256kbps-1.m4s",
|
||||
("https://hostname/period/adaptationset/representation/init_audio_256kbps.m4s", expected_availability),
|
||||
("https://hostname/period/adaptationset/representation/media_audio_256kbps-1.m4s", expected_availability),
|
||||
],
|
||||
[
|
||||
"https://other/init_audio_320kbps.m4s",
|
||||
"https://other/media_audio_320kbps-1.m4s",
|
||||
("https://other/init_audio_320kbps.m4s", expected_availability),
|
||||
("https://other/media_audio_320kbps-1.m4s", expected_availability),
|
||||
],
|
||||
]
|
||||
|
||||
|
|
Loading…
Reference in New Issue