From 40104be738bf71c507c7ba579cd5ab8cbac4acb6 Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Wed, 27 May 2026 12:12:42 -0600 Subject: [PATCH] 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). --- unshackle/core/manifests/dash.py | 33 +++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/unshackle/core/manifests/dash.py b/unshackle/core/manifests/dash.py index ca8b35f..0890cce 100644 --- a/unshackle/core/manifests/dash.py +++ b/unshackle/core/manifests/dash.py @@ -7,7 +7,7 @@ import math import re import shutil import sys -from copy import copy +from copy import deepcopy from functools import partial from pathlib import Path from typing import Any, Callable, Optional, Union @@ -587,6 +587,32 @@ class DASH: return False 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 def _get_period_segments( period: Element, @@ -621,9 +647,7 @@ class DASH: period_duration = period.get("duration") or manifest.get("mediaPresentationDuration") init_data: Optional[bytes] = None - segment_template = representation.find("SegmentTemplate") - if segment_template is None: - segment_template = adaptation_set.find("SegmentTemplate") + segment_template = DASH._merge_segment_templates(adaptation_set, representation) segment_list = representation.find("SegmentList") if segment_list is None: @@ -639,7 +663,6 @@ class DASH: track_kid: Optional[UUID] = None if segment_template is not None: - segment_template = copy(segment_template) start_number = int(segment_template.get("startNumber") or 1) end_number = int(segment_template.get("endNumber") or 0) or None segment_timeline = segment_template.find("SegmentTimeline")