mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-11 19:45:06 +00:00
fix(dash): resolve representations by adaptation set, not id alone
Representation ids are only unique within an AdaptationSet, so re-finding a track by id could download a sibling set's stream (audio description in place of dialog). Applies to download_track and extract_dash.
This commit is contained in:
@@ -349,20 +349,16 @@ class DASH:
|
||||
if period_count > 1:
|
||||
log.debug(f"Multi-period manifest detected with {period_count} content periods")
|
||||
|
||||
stored_period: Optional[Element] = track.data["dash"].get("period")
|
||||
|
||||
for period_idx, content_period in enumerate(content_periods):
|
||||
# Find the matching representation in this period
|
||||
matched_rep = None
|
||||
matched_as = None
|
||||
for as_ in content_period.findall("AdaptationSet"):
|
||||
if DASH.is_trick_mode(as_):
|
||||
continue
|
||||
for rep in as_.findall("Representation"):
|
||||
if rep.get("id") == rep_id:
|
||||
matched_rep = rep
|
||||
matched_as = as_
|
||||
break
|
||||
if matched_rep is not None:
|
||||
break
|
||||
if content_period is stored_period:
|
||||
matched_as, matched_rep = adaptation_set, representation
|
||||
else:
|
||||
matched_as, matched_rep = DASH.resolve_representation(content_period, rep_id, adaptation_set) or (
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
if matched_rep is None or matched_as is None:
|
||||
period_id = content_period.get("id", period_idx)
|
||||
@@ -934,6 +930,54 @@ class DASH:
|
||||
for prop in essential_props + supplemental_props
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def adaptation_set_key(adaptation_set: Element) -> tuple[Optional[str], ...]:
|
||||
"""Identity of an AdaptationSet, used to re-find its counterpart in another Period."""
|
||||
return tuple(
|
||||
adaptation_set.get(attr)
|
||||
for attr in ("contentType", "mimeType", "lang", "audioTrackId", "audioTrackSubtype", "id")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def resolve_representation(
|
||||
period: Element, rep_id: Optional[str], adaptation_set: Element
|
||||
) -> Optional[tuple[Element, Element]]:
|
||||
"""
|
||||
Find the (AdaptationSet, Representation) in another Period matching rep_id.
|
||||
|
||||
Representation ids are only unique within an AdaptationSet, so the set matching
|
||||
`adaptation_set`'s identity wins over an earlier sibling that reuses the same id
|
||||
for different content (e.g. audio description vs dialog).
|
||||
"""
|
||||
as_key = DASH.adaptation_set_key(adaptation_set)
|
||||
|
||||
def candidates(within: Element) -> list[tuple[Element, Element]]:
|
||||
found = []
|
||||
for as_ in within.findall("AdaptationSet"):
|
||||
if DASH.is_trick_mode(as_) or DASH.adaptation_set_key(as_) != as_key:
|
||||
continue
|
||||
for rep in as_.findall("Representation"):
|
||||
if rep.get("id") == rep_id:
|
||||
found.append((as_, rep))
|
||||
break
|
||||
return found
|
||||
|
||||
matches = candidates(period)
|
||||
if matches:
|
||||
parent = adaptation_set.getparent()
|
||||
siblings = [as_ for as_, _ in candidates(parent)] if parent is not None else []
|
||||
ordinal = siblings.index(adaptation_set) if adaptation_set in siblings else 0
|
||||
return matches[min(ordinal, len(matches) - 1)]
|
||||
|
||||
for as_ in period.findall("AdaptationSet"):
|
||||
if DASH.is_trick_mode(as_):
|
||||
continue
|
||||
for rep in as_.findall("Representation"):
|
||||
if rep.get("id") == rep_id:
|
||||
return as_, rep
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_descriptive(adaptation_set: Element) -> bool:
|
||||
"""Check if contents of Adaptation Set is Descriptive."""
|
||||
|
||||
@@ -278,24 +278,22 @@ def extract_dash(track: "Track", session: Union[Session, RnetSession]) -> list[S
|
||||
|
||||
data = track.data["dash"]
|
||||
manifest = data["manifest"]
|
||||
rep_id = data.get("representation_id") or data["representation"].get("id")
|
||||
adaptation_set = data["adaptation_set"]
|
||||
representation = data["representation"]
|
||||
rep_id = data.get("representation_id") or representation.get("id")
|
||||
filtered_period_ids = data.get("filtered_period_ids", [])
|
||||
stored_period = data.get("period")
|
||||
track_url = track.url if isinstance(track.url, str) else track.url[0]
|
||||
|
||||
content_periods = [p for p in manifest.findall("Period") if DASH._is_content_period(p, filtered_period_ids)]
|
||||
|
||||
raw_segments: list[tuple[str, Optional[str]]] = []
|
||||
for period in content_periods:
|
||||
matched_rep = matched_as = None
|
||||
for as_ in period.findall("AdaptationSet"):
|
||||
if DASH.is_trick_mode(as_):
|
||||
continue
|
||||
for rep in as_.findall("Representation"):
|
||||
if rep.get("id") == rep_id:
|
||||
matched_rep, matched_as = rep, as_
|
||||
break
|
||||
if matched_rep is not None:
|
||||
break
|
||||
# rep ids are only unique within an AdaptationSet, so resolve like DASH.download_track
|
||||
if period is stored_period:
|
||||
matched_as, matched_rep = adaptation_set, representation
|
||||
else:
|
||||
matched_as, matched_rep = DASH.resolve_representation(period, rep_id, adaptation_set) or (None, None)
|
||||
if matched_rep is None or matched_as is None:
|
||||
continue
|
||||
|
||||
|
||||
Reference in New Issue
Block a user