mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user