streamlink/src/streamlink/stream/dash_manifest.py

1005 lines
33 KiB
Python

import copy
import dataclasses
import logging
import math
import re
from collections import defaultdict
from contextlib import contextmanager
from datetime import datetime, timedelta
from itertools import count, repeat
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Dict,
Iterator,
List,
Optional,
Sequence,
Set,
Tuple,
Type,
TypeVar,
Union,
overload,
)
from urllib.parse import urljoin, urlparse, urlsplit, urlunparse, urlunsplit
from isodate import Duration, parse_datetime, parse_duration # type: ignore[import]
# noinspection PyProtectedMember
from lxml.etree import _Attrib, _Element
from streamlink.utils.times import UTC, fromtimestamp, now
if TYPE_CHECKING: # pragma: no cover
from typing_extensions import Literal
log = logging.getLogger(__name__)
EPOCH_START = fromtimestamp(0)
ONE_SECOND = timedelta(seconds=1)
SEGMENT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
@dataclasses.dataclass
class Segment:
url: str
number: Optional[int] = None
duration: Optional[float] = None
available_at: datetime = EPOCH_START
init: bool = False
content: bool = True
byterange: Optional[Tuple[int, Optional[int]]] = None
@property
def name(self) -> str:
if self.init and not self.content:
return "initialization"
if self.number is not None:
return str(self.number)
return Path(urlparse(self.url).path).resolve().name
@property
def available_in(self) -> float:
return max(0.0, (self.available_at - now()).total_seconds())
@property
def availability(self) -> str:
return f"{self.available_at.strftime(SEGMENT_TIME_FORMAT)} / {now().strftime(SEGMENT_TIME_FORMAT)}"
@dataclasses.dataclass
class TimelineSegment:
t: int
d: int
def _identity(x):
return x
def datetime_to_seconds(dt):
return (dt - EPOCH_START).total_seconds()
def count_dt(firstval: Optional[datetime] = None, step: timedelta = ONE_SECOND) -> Iterator[datetime]:
current = now() if firstval is None else firstval
while True:
yield current
current += step
@contextmanager
def freeze_timeline(mpd):
timelines = copy.copy(mpd.timelines)
yield
mpd.timelines = timelines
_re_segment_template = re.compile(r"(.*?)\$(\w+)(?:%([\w.]+))?\$")
class MPDParsers:
@staticmethod
def bool_str(v: str) -> bool:
return v.lower() == "true"
@staticmethod
def type(mpdtype: "Literal['static', 'dynamic']") -> "Literal['static', 'dynamic']":
if mpdtype not in ("static", "dynamic"):
raise MPDParsingError("@type must be static or dynamic")
return mpdtype
@staticmethod
def duration(duration: str) -> Union[timedelta, Duration]:
return parse_duration(duration)
@staticmethod
def datetime(dt: str) -> datetime:
return parse_datetime(dt).replace(tzinfo=UTC)
@staticmethod
def segment_template(url_template: str) -> Callable[..., str]:
end = 0
res = ""
for m in _re_segment_template.finditer(url_template):
_, end = m.span()
res += f"{m[1]}{{{m[2]}{f':{m[3]}' if m[3] else ''}}}"
return f"{res}{url_template[end:]}".format
@staticmethod
def frame_rate(frame_rate: str) -> float:
if "/" not in frame_rate:
return float(frame_rate)
a, b = frame_rate.split("/")
return float(a) / float(b)
@staticmethod
def timedelta(timescale: float = 1):
def _timedelta(seconds):
return timedelta(seconds=int(float(seconds) / float(timescale)))
return _timedelta
@staticmethod
def range(range_spec: str) -> Tuple[int, Optional[int]]:
r = range_spec.split("-")
if len(r) != 2:
raise MPDParsingError("Invalid byte-range-spec")
start, end = int(r[0]), r[1] and int(r[1]) or None
return start, end and ((end - start) + 1)
class MPDParsingError(Exception):
pass
TMPDNode = TypeVar("TMPDNode", bound="MPDNode", covariant=True)
TAttrDefault = TypeVar("TAttrDefault", Any, None)
TAttrParseResult = TypeVar("TAttrParseResult")
TTimelineIdent = Tuple[Optional[str], Optional[str], str]
class MPDNode:
__tag__: ClassVar[str]
parent: "MPDNode"
def __init__(self, node: _Element, root: "MPD", parent: "MPDNode", **kwargs) -> None:
self.node = node
self.root = root
self.parent = parent
self._base_url = kwargs.get("base_url")
self.attributes: Set[str] = set()
if self.__tag__ and self.node.tag.lower() != self.__tag__.lower():
raise MPDParsingError(f"Root tag did not match the expected tag: {self.__tag__}")
@property
def attrib(self) -> _Attrib:
return self.node.attrib
@property
def text(self) -> Optional[str]:
return self.node.text
def __str__(self):
return f"<{self.__tag__} {' '.join(f'@{attr}={getattr(self, attr)}' for attr in self.attributes)}>"
@overload
def attr( # type: ignore[misc] # "Overloaded function signatures 1 and 2 overlap with incompatible return types"
self,
key: str,
parser: None = None,
default: None = None,
required: bool = False,
inherited: Optional[Union[Type[TMPDNode], Sequence[Type[TMPDNode]]]] = None,
) -> Optional[str]: # pragma: no cover
pass
@overload
def attr(
self,
key: str,
parser: None,
default: TAttrDefault,
required: bool = False,
inherited: Optional[Union[Type[TMPDNode], Sequence[Type[TMPDNode]]]] = None,
) -> TAttrDefault: # pragma: no cover
pass
@overload
def attr(
self,
key: str,
parser: Callable[[Any], TAttrParseResult],
default: None = None,
required: bool = False,
inherited: Optional[Union[Type[TMPDNode], Sequence[Type[TMPDNode]]]] = None,
) -> Optional[TAttrParseResult]: # pragma: no cover
pass
@overload
def attr(
self,
key: str,
parser: Callable[[Any], TAttrParseResult],
default: TAttrDefault,
required: bool = False,
inherited: Optional[Union[Type[TMPDNode], Sequence[Type[TMPDNode]]]] = None,
) -> Union[TAttrParseResult, TAttrDefault]: # pragma: no cover
pass
def attr(self, key, parser=None, default=None, required=False, inherited=None):
self.attributes.add(key)
if key in self.attrib:
value = self.attrib.get(key)
if parser and callable(parser):
return parser(value)
else:
return value
elif inherited:
value = self.walk_back_get_attr(key, inherited)
if value is not None:
return value
if required: # pragma: no cover
raise MPDParsingError(f"Could not find required attribute {self.__tag__}@{key} ")
return default
def children(
self,
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, **kwargs)
for i, child in enumerate(children)
]
def only_child(
self,
cls: Type[TMPDNode],
minimum: int = 0,
**kwargs,
) -> Optional[TMPDNode]:
children = self.children(cls, minimum=minimum, maximum=1, **kwargs)
return children[0] if len(children) else None
def walk_back(
self,
cls: Optional[Union[Type[TMPDNode], Sequence[Type[TMPDNode]]]] = None,
mapper: Callable[["MPDNode"], Optional["MPDNode"]] = _identity,
) -> Iterator["MPDNode"]:
node = self.parent
while node:
if cls is None or isinstance(node, cls): # type: ignore[arg-type]
n = mapper(node) # type: ignore[arg-type]
if n is not None:
yield n
node = node.parent
def walk_back_get_attr(
self,
attr: str,
cls: Optional[Union[Type[TMPDNode], Sequence[Type[TMPDNode]]]] = None,
mapper: Callable[["MPDNode"], Optional["MPDNode"]] = _identity,
) -> Optional[Any]:
for ancestor in self.walk_back(cls, mapper):
value = getattr(ancestor, attr, None)
if value is not None:
return value
@property
def base_url(self):
base_url = self._base_url
if hasattr(self, "baseURLs") and len(self.baseURLs):
base_url = BaseURL.join(base_url, self.baseURLs[0].url)
return base_url
class MPD(MPDNode):
"""
Represents the MPD as a whole
Should validate the XML input and provide methods to get segment URLs for each Period, AdaptationSet and Representation.
"""
__tag__ = "MPD"
parent: None # type: ignore[assignment]
timelines: Dict[TTimelineIdent, int]
def __init__(self, *args, url: Optional[str] = None, **kwargs) -> None:
# top level has no parent
kwargs["root"] = self
kwargs["parent"] = None
super().__init__(*args, **kwargs)
# parser attributes
self.url = url
self.timelines = defaultdict(lambda: -1)
self.timelines.update(kwargs.pop("timelines", {}))
self.id = self.attr("id")
self.profiles = self.attr(
"profiles",
required=True,
)
self.type = self.attr(
"type",
parser=MPDParsers.type,
default="static",
)
self.minimumUpdatePeriod = self.attr(
"minimumUpdatePeriod",
parser=MPDParsers.duration,
default=timedelta(),
)
self.minBufferTime: Union[timedelta, Duration] = self.attr(
"minBufferTime",
parser=MPDParsers.duration,
required=True,
)
self.timeShiftBufferDepth = self.attr(
"timeShiftBufferDepth",
parser=MPDParsers.duration,
)
self.availabilityStartTime = self.attr(
"availabilityStartTime",
parser=MPDParsers.datetime,
default=EPOCH_START,
required=self.type == "dynamic",
)
self.publishTime = self.attr(
"publishTime",
parser=MPDParsers.datetime,
required=self.type == "dynamic",
)
self.availabilityEndTime = self.attr(
"availabilityEndTime",
parser=MPDParsers.datetime,
)
self.mediaPresentationDuration = self.attr(
"mediaPresentationDuration",
parser=MPDParsers.duration,
default=timedelta(),
)
self.suggestedPresentationDelay = self.attr(
"suggestedPresentationDelay",
parser=MPDParsers.duration,
# if there is no delay, use a delay of 3 seconds
# TODO: add a customizable parameter for this
default=timedelta(seconds=3),
)
# parse children
location = self.children(Location)
self.location = location[0] if location else None
if self.location:
self.url = self.location.text or ""
urlp = list(urlparse(self.url))
if urlp[2]:
urlp[2], _ = urlp[2].rsplit("/", 1)
self._base_url = urlunparse(urlp)
self.baseURLs = self.children(BaseURL)
self.periods = self.children(Period, minimum=1)
self.programInformation = self.children(ProgramInformation)
def get_representation(self, ident: TTimelineIdent) -> Optional["Representation"]:
"""
Find the first Representation instance with a matching ident
"""
for period in self.periods:
for adaptationset in period.adaptationSets:
for representation in adaptationset.representations:
if representation.ident == ident:
return representation
class ProgramInformation(MPDNode):
__tag__ = "ProgramInformation"
class BaseURL(MPDNode):
__tag__ = "BaseURL"
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.url = (self.text or "").strip()
@property
def is_absolute(self) -> bool:
return bool(urlparse(self.url).scheme)
@staticmethod
def join(url: str, other: str) -> str:
# if the other URL is an absolute url, then return that
if urlparse(other).scheme:
return other
elif url:
parts = list(urlsplit(url))
if not parts[2].endswith("/"):
parts[2] += "/"
url = urlunsplit(parts)
return urljoin(url, other)
else:
return other
class Location(MPDNode):
__tag__ = "Location"
class Period(MPDNode):
__tag__ = "Period"
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.i = kwargs.get("i", 0)
self.id = self.attr("id")
self.bitstreamSwitching = self.attr(
"bitstreamSwitching",
parser=MPDParsers.bool_str,
)
self.duration = self.attr(
"duration",
parser=MPDParsers.duration,
default=timedelta(),
)
self.start = self.attr(
"start",
parser=MPDParsers.duration,
default=timedelta(),
)
# anchor time for segment availability
offset = self.start if self.root.type == "dynamic" else timedelta()
self.availabilityStartTime = self.root.availabilityStartTime + offset
# TODO: Early Access Periods
self.baseURLs = self.children(BaseURL)
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.assetIdentifier = self.only_child(AssetIdentifier)
self.eventStream = self.children(EventStream)
self.subset = self.children(Subset)
class AssetIdentifier(MPDNode):
__tag__ = "AssetIdentifier"
class Subset(MPDNode):
__tag__ = "Subset"
class EventStream(MPDNode):
__tag__ = "EventStream"
class _RepresentationBaseType(MPDNode):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
# mimeType must be set on the AdaptationSet or Representation
self.mimeType: str = self.attr( # type: ignore[assignment]
"mimeType",
required=type(self) is Representation,
inherited=_RepresentationBaseType,
)
self.profiles = self.attr(
"profiles",
inherited=_RepresentationBaseType,
)
self.width = self.attr(
"width",
parser=int,
inherited=_RepresentationBaseType,
)
self.height = self.attr(
"height",
parser=int,
inherited=_RepresentationBaseType,
)
self.sar = self.attr(
"sar",
inherited=_RepresentationBaseType,
)
self.frameRate = self.attr(
"frameRate",
parser=MPDParsers.frame_rate,
inherited=_RepresentationBaseType,
)
self.audioSamplingRate = self.attr(
"audioSamplingRate",
parser=int,
inherited=_RepresentationBaseType,
)
self.codecs = self.attr(
"codecs",
inherited=_RepresentationBaseType,
)
self.scanType = self.attr(
"scanType",
inherited=_RepresentationBaseType,
)
self.contentProtections = self.children(ContentProtection)
class AdaptationSet(_RepresentationBaseType):
__tag__ = "AdaptationSet"
parent: Period
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.id = self.attr("id")
self.group = self.attr("group")
self.lang = self.attr("lang")
self.contentType = self.attr("contentType")
self.par = self.attr("par")
self.minBandwidth = self.attr("minBandwidth", parser=int)
self.maxBandwidth = self.attr("maxBandwidth", parser=int)
self.minWidth = self.attr("minWidth", parser=int)
self.maxWidth = self.attr("maxWidth", parser=int)
self.minHeight = self.attr("minHeight", parser=int)
self.maxHeight = self.attr("maxHeight", parser=int)
self.minFrameRate = self.attr("minFrameRate", parser=MPDParsers.frame_rate)
self.maxFrameRate = self.attr("maxFrameRate", parser=MPDParsers.frame_rate)
self.segmentAlignment = self.attr(
"segmentAlignment",
parser=MPDParsers.bool_str,
default=False,
)
self.subsegmentAlignment = self.attr(
"subsegmentAlignment",
parser=MPDParsers.bool_str,
default=False,
)
self.subsegmentStartsWithSAP = self.attr(
"subsegmentStartsWithSAP",
parser=int,
default=0,
)
self.bitstreamSwitching = self.attr(
"bitstreamSwitching",
parser=MPDParsers.bool_str,
)
self.baseURLs = self.children(BaseURL)
self.segmentBase = self.only_child(SegmentBase, period=self.parent)
self.segmentList = self.only_child(SegmentList, period=self.parent)
self.segmentTemplate = self.only_child(SegmentTemplate, period=self.parent)
self.representations = self.children(Representation, minimum=1, period=self.parent)
class Representation(_RepresentationBaseType):
__tag__ = "Representation"
parent: AdaptationSet
def __init__(self, *args, period: Period, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.period = period
self.id: str = self.attr( # type: ignore[assignment]
"id",
required=True,
)
self.bandwidth: float = self.attr( # type: ignore[assignment]
"bandwidth",
parser=lambda b: float(b) / 1000.0,
required=True,
)
self.ident = self.parent.parent.id, self.parent.id, self.id
self.baseURLs = self.children(BaseURL)
self.subRepresentations = self.children(SubRepresentation)
self.segmentBase = self.only_child(SegmentBase, period=self.period)
self.segmentList = self.only_child(SegmentList, period=self.period)
self.segmentTemplate = self.only_child(SegmentTemplate, period=self.period)
@property
def lang(self):
return self.parent.lang
@property
def bandwidth_rounded(self) -> float:
return round(self.bandwidth, 1 - int(math.log10(self.bandwidth)))
def segments(self, **kwargs) -> Iterator[Segment]:
"""
Segments are yielded when they are available
Segments appear on a timeline, for dynamic content they are only available at a certain time
and sometimes for a limited time. For static content they are all available at the same time.
:param kwargs: extra args to pass to the segment template
:return: yields Segments
"""
# segmentBase = self.segmentBase or self.walk_back_get_attr("segmentBase")
segmentList = self.segmentList or self.walk_back_get_attr("segmentList")
segmentTemplate = self.segmentTemplate or self.walk_back_get_attr("segmentTemplate")
if segmentTemplate:
yield from segmentTemplate.segments(
self.ident,
self.base_url,
RepresentationID=self.id,
Bandwidth=int(self.bandwidth * 1000),
**kwargs,
)
elif segmentList:
yield from segmentList.segments()
else:
yield Segment(
url=self.base_url,
number=None,
duration=self.period.duration.total_seconds() or self.root.mediaPresentationDuration.total_seconds(),
available_at=self.period.availabilityStartTime,
init=True,
content=True,
byterange=None,
)
class SubRepresentation(_RepresentationBaseType):
__tag__ = "SubRepresentation"
class _SegmentBaseType(MPDNode):
parent: Union[Period, AdaptationSet, Representation]
_ancestors = (Period, AdaptationSet, Representation)
def __init__(self, *args, period: "Period", **kwargs) -> None:
super().__init__(*args, **kwargs)
self.period = period
self.timescale: int = self.attr(
"timescale",
parser=int,
default=self._find_default("timescale", 1),
)
self.presentationTimeOffset: timedelta = self.attr(
"presentationTimeOffset",
parser=MPDParsers.timedelta(self.timescale),
default=self._find_default("presentationTimeOffset", timedelta()),
)
self.availabilityTimeOffset: timedelta = self.attr(
"availabilityTimeOffset",
parser=MPDParsers.timedelta(self.timescale),
default=self._find_default("availabilityTimeOffset", timedelta()),
)
self.initialization = self.only_child(Initialization) or self._find_default("initialization")
def _find_default(self, attr: str, default: TAttrDefault = None) -> Union[TAttrDefault, Any]:
"""Find default values from nodes of the same type on ancestor nodes"""
# the node attribute on each ancestor is named after its node tag, with the first character being lowercase
nodeattr = f"{self.__tag__[0].lower()}{self.__tag__[1:]}"
# start with the parent node, to avoid an unnecessary failed lookup on the current node
value = self.parent.walk_back_get_attr(
attr,
self._ancestors,
lambda node: getattr(node, nodeattr, None),
)
return default if value is None else value
class _MultipleSegmentBaseType(_SegmentBaseType):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.duration = self.attr(
"duration",
parser=int,
default=self._find_default("duration"),
)
self.startNumber: int = self.attr(
"startNumber",
parser=int,
default=self._find_default("startNumber", 1),
)
self.duration_seconds = self.duration / self.timescale if self.duration else None
self.segmentTimeline = self.only_child(SegmentTimeline) or self._find_default("segmentTimeline")
class SegmentBase(_SegmentBaseType):
__tag__ = "SegmentBase"
class SegmentList(_MultipleSegmentBaseType):
__tag__ = "SegmentList"
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.segmentURLs = self.children(SegmentURL)
def segments(self) -> Iterator[Segment]:
if self.initialization: # pragma: no branch
yield Segment(
url=self.make_url(self.initialization.source_url),
number=None,
duration=None,
available_at=self.period.availabilityStartTime,
init=True,
content=False,
byterange=self.initialization.range,
)
for number, segment_url in enumerate(self.segmentURLs, self.startNumber):
yield Segment(
url=self.make_url(segment_url.media),
number=number,
duration=self.duration_seconds,
available_at=self.period.availabilityStartTime,
init=False,
content=True,
byterange=segment_url.media_range,
)
def make_url(self, url: Optional[str]) -> str:
return BaseURL.join(self.base_url, url) if url else self.base_url
class SegmentTemplate(_MultipleSegmentBaseType):
__tag__ = "SegmentTemplate"
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.fmt_initialization = self.attr(
"initialization",
parser=MPDParsers.segment_template,
)
self.fmt_media = self.attr(
"media",
parser=MPDParsers.segment_template,
)
def segments(self, ident: TTimelineIdent, base_url: str, **kwargs) -> Iterator[Segment]:
if kwargs.pop("init", True): # pragma: no branch
init_url = self.format_initialization(base_url, **kwargs)
if init_url: # pragma: no branch
yield Segment(
url=init_url,
number=None,
duration=None,
available_at=self.period.availabilityStartTime,
init=True,
content=False,
byterange=None,
)
for media_url, number, available_at in self.format_media(ident, base_url, **kwargs):
yield Segment(
url=media_url,
number=number,
duration=self.duration_seconds,
available_at=available_at,
init=False,
content=True,
byterange=None,
)
@staticmethod
def make_url(base_url: str, url: str) -> str:
return BaseURL.join(base_url, url)
def segment_numbers(self) -> Iterator[Tuple[int, datetime]]:
"""
yield the segment number and when it will be available.
There are two cases for segment number generation, "static" and "dynamic":
In the case of static streams, the segment number starts at the startNumber and counts
up to the number of segments that are represented by the periods-duration.
In the case of dynamic streams, the segments should appear at the specified time.
In the simplest case, the segment number is based on the time since the availabilityStartTime.
"""
if not self.duration_seconds: # pragma: no cover
raise MPDParsingError("Unknown segment durations: missing duration/timescale attributes on SegmentTemplate")
number_iter: Union[Iterator[int], Sequence[int]]
available_iter: Iterator[datetime]
if self.root.type == "static":
available_iter = repeat(self.period.availabilityStartTime)
duration = self.period.duration.total_seconds() or self.root.mediaPresentationDuration.total_seconds()
if duration:
number_iter = range(self.startNumber, int(duration / self.duration_seconds) + 1)
else:
number_iter = count(self.startNumber)
else:
current_time = now()
since_start = current_time - self.period.availabilityStartTime - self.presentationTimeOffset
suggested_delay = self.root.suggestedPresentationDelay
buffer_time = self.root.minBufferTime
# Segment number
seconds_offset = (since_start - suggested_delay - buffer_time).total_seconds()
number_offset = max(0, int(seconds_offset / self.duration_seconds))
number_iter = count(self.startNumber + number_offset)
# Segment availability time
available_offset = timedelta(seconds=number_offset * self.duration_seconds)
available_start = self.period.availabilityStartTime + available_offset
available_iter = count_dt(
available_start,
timedelta(seconds=self.duration_seconds),
)
log.debug(f"Stream start: {self.period.availabilityStartTime}")
log.debug(f"Current time: {current_time}")
log.debug(f"Availability: {available_start}")
log.debug("; ".join([
f"presentationTimeOffset: {self.presentationTimeOffset}",
f"suggestedPresentationDelay: {self.root.suggestedPresentationDelay}",
f"minBufferTime: {self.root.minBufferTime}",
]))
log.debug("; ".join([
f"segmentDuration: {self.duration_seconds}",
f"segmentStart: {self.startNumber}",
f"segmentOffset: {number_offset} ({seconds_offset}s)",
]))
yield from zip(number_iter, available_iter)
def segment_timeline(self, ident: TTimelineIdent) -> Iterator[Tuple[int, TimelineSegment, datetime]]:
if not self.segmentTimeline: # pragma: no cover
raise MPDParsingError("Missing SegmentTimeline in SegmentTemplate")
if self.root.type == "static":
yield from zip(count(self.startNumber), self.segmentTimeline.segments, repeat(self.period.availabilityStartTime))
else:
time = self.root.timelines[ident]
is_initial = time == -1
publish_time = self.root.publishTime or EPOCH_START
threshold = publish_time - self.root.suggestedPresentationDelay
# transform the timeline into a segment list
timeline = []
available_at = publish_time
# the last segment in the timeline is the most recent one
# so, work backwards and calculate when each of the segments was
# available, based on the durations relative to the publish-time
for number, segment in reversed(list(zip(count(self.startNumber), self.segmentTimeline.segments))):
# stop once the suggestedPresentationDelay is reached on the first manifest parsing
# or when a segment with a lower or equal time value was already returned from an earlier manifest
if is_initial and available_at <= threshold or segment.t <= time:
break
timeline.append((number, segment, available_at))
available_at -= timedelta(seconds=segment.d / self.timescale)
# return the segments in chronological order
for number, segment, available_at in reversed(timeline):
self.root.timelines[ident] = segment.t
yield number, segment, available_at
def format_initialization(self, base_url: str, **kwargs) -> Optional[str]:
if self.fmt_initialization is not None: # pragma: no branch
return self.make_url(base_url, self.fmt_initialization(**kwargs))
def format_media(self, ident: TTimelineIdent, base_url: str, **kwargs) -> Iterator[Tuple[str, int, datetime]]:
if self.fmt_media is None: # pragma: no cover
return
if not self.segmentTimeline:
log.debug(f"Generating segment numbers for {self.root.type} playlist: {ident!r}")
for number, available_at in self.segment_numbers():
url = self.make_url(base_url, self.fmt_media(Number=number, **kwargs))
yield url, number, available_at
else:
log.debug(f"Generating segment timeline for {self.root.type} playlist: {ident!r}")
for number, segment, available_at in self.segment_timeline(ident):
url = self.make_url(base_url, self.fmt_media(Time=segment.t, Number=number, **kwargs))
yield url, number, available_at
class SegmentTimeline(MPDNode):
__tag__ = "SegmentTimeline"
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.timescale = self.walk_back_get_attr("timescale")
self.timeline_segments = self.children(_TimelineSegment)
@property
def segments(self) -> Iterator[TimelineSegment]:
t = 0
for tsegment in self.timeline_segments:
if t == 0 and tsegment.t is not None:
t = tsegment.t
# check the start time from MPD
for _ in range(tsegment.r + 1):
yield TimelineSegment(t, tsegment.d)
t += tsegment.d
class _TimelineSegment(MPDNode):
__tag__ = "S"
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.t = self.attr("t", parser=int)
self.d: int = self.attr("d", parser=int, required=True) # type: ignore[assignment]
self.r = self.attr("r", parser=int, default=0)
class Initialization(MPDNode):
__tag__ = "Initialization"
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.source_url = self.attr("sourceURL")
self.range = self.attr(
"range",
parser=MPDParsers.range,
)
class SegmentURL(MPDNode):
__tag__ = "SegmentURL"
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.media = self.attr("media")
self.media_range = self.attr(
"mediaRange",
parser=MPDParsers.range,
)
class ContentProtection(MPDNode):
__tag__ = "ContentProtection"
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.schemeIdUri = self.attr("schemeIdUri")
self.value = self.attr("value")
self.default_KID = self.attr("default_KID")