fix(dash): inherit SegmentTemplate attributes across AdaptationSet/Representation

A SegmentTemplate can sit at both the AdaptationSet and Representation levels, with shared attrs like @timescale only on the outer node. The parser picked one or the other, dropping the outer node when an inner existed - so @timescale defaulted to 1 and only the first segment was emitted.

Merge instead: Representation node as base, inheriting any attribute or SegmentTimeline it omits from the AdaptationSet node (per ISO/IEC 23009-1).
This commit is contained in:
imSp4rky
2026-05-27 12:12:42 -06:00
parent 96fd971af0
commit 40104be738

View File

@@ -7,7 +7,7 @@ import math
import re import re
import shutil import shutil
import sys import sys
from copy import copy from copy import deepcopy
from functools import partial from functools import partial
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Optional, Union from typing import Any, Callable, Optional, Union
@@ -587,6 +587,32 @@ class DASH:
return False return False
return True return True
@staticmethod
def _merge_segment_templates(adaptation_set: Element, representation: Element) -> Optional[Element]:
"""
Build the effective SegmentTemplate for a Representation by cascading the
AdaptationSet > Representation levels (ISO/IEC 23009-1 5.3.9.1).
The Representation-level node, when present, is the base; attributes and the
SegmentTimeline child it does not declare are inherited from the AdaptationSet-level
node. Returns None if no SegmentTemplate exists at either level.
"""
levels = [node.find("SegmentTemplate") for node in (adaptation_set, representation)]
present = [node for node in levels if node is not None]
if not present:
return None
merged = deepcopy(present[-1])
for ancestor in reversed(present[:-1]):
for attr, value in ancestor.attrib.items():
if merged.get(attr) is None:
merged.set(attr, value)
if merged.find("SegmentTimeline") is None:
timeline = ancestor.find("SegmentTimeline")
if timeline is not None:
merged.append(deepcopy(timeline))
return merged
@staticmethod @staticmethod
def _get_period_segments( def _get_period_segments(
period: Element, period: Element,
@@ -621,9 +647,7 @@ class DASH:
period_duration = period.get("duration") or manifest.get("mediaPresentationDuration") period_duration = period.get("duration") or manifest.get("mediaPresentationDuration")
init_data: Optional[bytes] = None init_data: Optional[bytes] = None
segment_template = representation.find("SegmentTemplate") segment_template = DASH._merge_segment_templates(adaptation_set, representation)
if segment_template is None:
segment_template = adaptation_set.find("SegmentTemplate")
segment_list = representation.find("SegmentList") segment_list = representation.find("SegmentList")
if segment_list is None: if segment_list is None:
@@ -639,7 +663,6 @@ class DASH:
track_kid: Optional[UUID] = None track_kid: Optional[UUID] = None
if segment_template is not None: if segment_template is not None:
segment_template = copy(segment_template)
start_number = int(segment_template.get("startNumber") or 1) start_number = int(segment_template.get("startNumber") or 1)
end_number = int(segment_template.get("endNumber") or 0) or None end_number = int(segment_template.get("endNumber") or 0) or None
segment_timeline = segment_template.find("SegmentTimeline") segment_timeline = segment_template.find("SegmentTimeline")