feat(video): normalize SPS VUI to match manifest-derived range

Some services ship HDR10/HLG bitstreams whose SPS VUI still carries BT.709 colour primaries/transfer/matrix, causing mediainfo and downstream players to mis-classify the file. The manifest-derived `Video.range` is the source of truth; rewrite the VUI with ffmpeg h264_metadata/hevc_metadata BSF after repackage and before mux. Skips SDR, DV, and HYBRID; no-op when the VUI is already correct.
This commit is contained in:
imSp4rky
2026-05-16 13:55:05 -06:00
parent ead88fe066
commit f26f9bcbe2
2 changed files with 68 additions and 0 deletions

View File

@@ -2310,6 +2310,13 @@ class dl:
# we don't want to fill up the log with "Repacked x track"
self.log.info("Repacked one or more tracks with FFMPEG")
with console.status("Normalizing video VUI..."):
for track in title.tracks.videos:
try:
track.normalize_vui()
except Exception as e: # noqa: BLE001
self.log.warning(f"VUI normalization skipped for {track.id}: {e}")
muxed_paths = []
muxed_audio_codecs: dict[Path, Optional[Audio.Codec]] = {}
append_audio_codec_suffix = True

View File

@@ -367,6 +367,67 @@ class Video(Track):
self.path = output_path
original_path.unlink()
def normalize_vui(self) -> bool:
"""Rewrite SPS VUI colour metadata to match ``self.range``.
Some services ship HDR10/HLG bitstreams with stale BT.709 VUI, which makes
downstream tools mis-classify the file. The manifest-derived range is the
source of truth. Skips SDR, DV, and HYBRID. Returns True if the bitstream
was rewritten.
"""
if not self.path or not self.path.exists():
return False
if self.codec not in (Video.Codec.AVC, Video.Codec.HEVC):
return False
if self.range in (Video.Range.SDR, Video.Range.DV, Video.Range.HYBRID):
return False
vui = {
Video.Range.HDR10: (9, 16, 9),
Video.Range.HDR10P: (9, 16, 9),
Video.Range.HLG: (9, 18, 9),
}.get(self.range)
if not vui:
return False
if not binaries.FFMPEG:
raise EnvironmentError('FFmpeg executable "ffmpeg" was not found but is required for this call.')
primaries, transfer, matrix = vui
filter_key = {Video.Codec.AVC: "h264_metadata", Video.Codec.HEVC: "hevc_metadata"}[self.codec]
bsf = (
f"{filter_key}=colour_primaries={primaries}"
f":transfer_characteristics={transfer}"
f":matrix_coefficients={matrix}"
)
original_path = self.path
output_path = original_path.with_stem(f"{original_path.stem}_vui")
try:
subprocess.run(
[
binaries.FFMPEG,
"-hide_banner",
"-loglevel",
"error",
"-i",
str(original_path),
"-codec",
"copy",
"-bsf:v",
bsf,
str(output_path),
],
check=True,
)
except subprocess.CalledProcessError:
output_path.unlink(missing_ok=True)
return False
self.path = output_path
original_path.unlink()
return True
def ccextractor(
self, track_id: Any, out_path: Union[Path, str], language: Language, original: bool = False
) -> Optional[Subtitle]: