fix(remote): match re-parsed manifest tracks by ID before attributes

Match once after every server manifest is parsed, and require range and bitrate in the video fallback, so a per-range ISM ladder no longer hands an HDR10 track the SDR quality level. Pass -xerror to the bsf ffmpeg calls so a failed rewrite keeps the original file instead of a near-empty one.
This commit is contained in:
imSp4rky
2026-09-01 15:06:29 -06:00
parent e00f6cca86
commit f8f58f86ef
4 changed files with 90 additions and 13 deletions
@@ -146,6 +146,35 @@ def test_match_track_by_attributes_when_id_missing() -> None:
assert match_track(remote, [local]) is local
def test_match_track_by_attributes_keeps_range_and_bitrate_apart() -> None:
sdr = deserialize_video(
{"id": "a", "codec": "HEVC", "width": 1920, "height": 960, "language": "en", "range": "SDR", "bitrate": 8000000}
)
hdr = deserialize_video(
{
"id": "b",
"codec": "HEVC",
"width": 1920,
"height": 960,
"language": "en",
"range": "HDR10",
"bitrate": 5000000,
}
)
remote = deserialize_video(
{
"id": "z",
"codec": "HEVC",
"width": 1920,
"height": 960,
"language": "en",
"range": "HDR10",
"bitrate": 5000000,
}
)
assert match_track(remote, [sdr, hdr]) is hdr
def test_match_track_no_candidates_returns_none() -> None:
remote = deserialize_video({"id": "X", "codec": "AVC", "width": 1, "height": 1})
assert match_track(remote, []) is None
@@ -287,3 +316,42 @@ def test_load_cache_files_wiring(tmp_path, monkeypatch) -> None:
# A profile with no credentials entry falls back to the default credential
sent = RemoteService.load_cache_files(stub, "missing")
assert set(sent) == {"tokens_default", "session_guid"}
def test_resolve_manifest_data_matches_across_per_range_manifests() -> None:
"""One ISM manifest per range. The HDR10 track must take the quality level from its
own manifest, not the same-sized one in the SDR manifest, in any manifest order."""
import base64
import zlib
from tests.core.test_ism_init import VIDEO_HEVC_PQ_CPD, VIDEO_HEVC_SDR_CPD
from tests.core.test_ism_range import manifest_xml
from unshackle.core.manifests import ISM
from unshackle.core.remote_service import resolve_manifest_data
urls = {"sdr": "https://x/sdr/manifest", "hdr": "https://x/hdr/manifest"}
xml = {"sdr": manifest_xml(VIDEO_HEVC_SDR_CPD), "hdr": manifest_xml(VIDEO_HEVC_PQ_CPD)}
local = {k: ISM.from_text(xml[k], url=urls[k]).to_tracks(language="en").videos[0] for k in xml}
assert local["sdr"].range == Video.Range.SDR and local["hdr"].range == Video.Range.HDR10
remote_hdr = deserialize_video(
{
"id": str(local["hdr"].id),
"codec": "HEVC",
"width": 3840,
"height": 2160,
"language": "en",
"range": "HDR10",
"bitrate_bps": 15000000,
}
)
tracks = build_tracks({"video": [], "audio": [], "subtitles": []})
tracks.videos.append(remote_hdr)
manifests = [
{"type": "ism", "url": urls[k], "data": base64.b64encode(zlib.compress(xml[k].encode())).decode()}
for k in ("sdr", "hdr")
]
resolve_manifest_data(tracks, manifests)
assert remote_hdr.data["ism"]["quality_level"].get("CodecPrivateData") == VIDEO_HEVC_PQ_CPD
+18 -12
View File
@@ -321,6 +321,7 @@ def resolve_manifest_data(tracks: Tracks, manifests: list) -> None:
log_m = logging.getLogger("remote_service")
all_tracks = list(tracks.videos) + list(tracks.audio) + list(tracks.subtitles)
local_all: list = []
for manifest_info in manifests:
m_type = manifest_info.get("type")
@@ -351,20 +352,20 @@ def resolve_manifest_data(tracks: Tracks, manifests: list) -> None:
else:
continue
local_all = list(local_tracks.videos) + list(local_tracks.audio) + list(local_tracks.subtitles)
for remote_track in all_tracks:
if remote_track.data.get(m_type):
continue
matched = match_track(remote_track, local_all)
if matched and matched.data.get(m_type):
remote_track.data.update(matched.data)
remote_track.descriptor = matched.descriptor
if matched.drm and not remote_track.drm:
remote_track.drm = matched.drm
local_all += list(local_tracks.videos) + list(local_tracks.audio) + list(local_tracks.subtitles)
except Exception as e:
log_m.warning("Failed to re-parse %s manifest from %s: %s", m_type, m_url, e)
for remote_track in all_tracks:
if any(remote_track.data.get(key) for key in ("dash", "ism")):
continue
matched = match_track(remote_track, local_all)
if matched and any(matched.data.get(key) for key in ("dash", "ism")):
remote_track.data.update(matched.data)
remote_track.descriptor = matched.descriptor
if matched.drm and not remote_track.drm:
remote_track.drm = matched.drm
def same_bitrate(local: Optional[int], remote: Optional[int]) -> bool:
"""Equal to the nearest kb/s. A server that predates ``bitrate_bps`` rounds to kb/s in transit."""
@@ -386,7 +387,12 @@ def match_track(remote_track: Track, local_tracks: list) -> Optional[Track]:
if lt.codec != remote_track.codec or str(lt.language) != str(remote_track.language):
continue
if hasattr(lt, "width") and hasattr(remote_track, "width"):
if lt.width == remote_track.width and lt.height == remote_track.height:
if (
lt.width == remote_track.width
and lt.height == remote_track.height
and lt.range == remote_track.range
and same_bitrate(lt.bitrate, remote_track.bitrate)
):
return lt
elif hasattr(lt, "channels") and hasattr(remote_track, "channels"):
if same_bitrate(lt.bitrate, remote_track.bitrate):
+2 -1
View File
@@ -1016,7 +1016,8 @@ class Track:
]
)
if bsf:
args.extend(["-bsf:v", bsf])
# ffmpeg exits 0 after dropping packets the bsf cannot parse
args.extend(["-bsf:v", bsf, "-xerror"])
args.append(str(output_path))
subprocess.run(
+2
View File
@@ -497,6 +497,8 @@ class Video(Track):
"-hide_banner",
"-loglevel",
"error",
# ffmpeg exits 0 after dropping packets the bsf cannot parse
"-xerror",
"-i",
str(original_path),
"-codec",