mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-14 21:15: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:
|
if period_count > 1:
|
||||||
log.debug(f"Multi-period manifest detected with {period_count} content periods")
|
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):
|
for period_idx, content_period in enumerate(content_periods):
|
||||||
# Find the matching representation in this period
|
if content_period is stored_period:
|
||||||
matched_rep = None
|
matched_as, matched_rep = adaptation_set, representation
|
||||||
matched_as = None
|
else:
|
||||||
for as_ in content_period.findall("AdaptationSet"):
|
matched_as, matched_rep = DASH.resolve_representation(content_period, rep_id, adaptation_set) or (
|
||||||
if DASH.is_trick_mode(as_):
|
None,
|
||||||
continue
|
None,
|
||||||
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 matched_rep is None or matched_as is None:
|
if matched_rep is None or matched_as is None:
|
||||||
period_id = content_period.get("id", period_idx)
|
period_id = content_period.get("id", period_idx)
|
||||||
@@ -934,6 +930,54 @@ class DASH:
|
|||||||
for prop in essential_props + supplemental_props
|
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
|
@staticmethod
|
||||||
def is_descriptive(adaptation_set: Element) -> bool:
|
def is_descriptive(adaptation_set: Element) -> bool:
|
||||||
"""Check if contents of Adaptation Set is Descriptive."""
|
"""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"]
|
data = track.data["dash"]
|
||||||
manifest = data["manifest"]
|
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", [])
|
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]
|
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)]
|
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]]] = []
|
raw_segments: list[tuple[str, Optional[str]]] = []
|
||||||
for period in content_periods:
|
for period in content_periods:
|
||||||
matched_rep = matched_as = None
|
# rep ids are only unique within an AdaptationSet, so resolve like DASH.download_track
|
||||||
for as_ in period.findall("AdaptationSet"):
|
if period is stored_period:
|
||||||
if DASH.is_trick_mode(as_):
|
matched_as, matched_rep = adaptation_set, representation
|
||||||
continue
|
else:
|
||||||
for rep in as_.findall("Representation"):
|
matched_as, matched_rep = DASH.resolve_representation(period, rep_id, adaptation_set) or (None, None)
|
||||||
if rep.get("id") == rep_id:
|
|
||||||
matched_rep, matched_as = rep, as_
|
|
||||||
break
|
|
||||||
if matched_rep is not None:
|
|
||||||
break
|
|
||||||
if matched_rep is None or matched_as is None:
|
if matched_rep is None or matched_as is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user