Add util to change video range flag losslessly

This is useful for some services. Some times there's a random stream with the wrong video range.
This commit is contained in:
rlaphoenix 2023-02-06 23:47:37 +00:00
parent 8c66e57175
commit 00f85f7206
3 changed files with 72 additions and 3 deletions

View File

@ -102,3 +102,72 @@ def crop(path: Path, aspect: str, letter: bool, offset: int, preview: bool) -> N
if ffmpeg_call.stdout:
ffmpeg_call.stdout.close()
ffmpeg_call.wait()
@util.command(name="range")
@click.argument("path", type=Path)
@click.option("--full/--limited", is_flag=True,
help="Full: 0..255, Limited: 16..235 (16..240 YUV luma)")
@click.option("-p", "--preview", is_flag=True, default=False,
help="Instantly preview the newly-set video range in MPV (or ffplay if mpv is unavailable).")
def range_(path: Path, full: bool, preview: bool) -> None:
"""
Losslessly set the Video Range flag to full or limited at the bit-stream level.
You may provide a path to a file, or a folder of mkv and/or mp4 files.
If you ever notice blacks not being quite black, and whites not being quite white,
then you're video may have the range set to the wrong value. Flip its range to the
opposite value and see if that fixes it.
"""
executable = get_binary_path("ffmpeg")
if not executable:
raise click.ClickException("FFmpeg executable \"ffmpeg\" not found but is required.")
if path.is_dir():
paths = list(path.glob("*.mkv")) + list(path.glob("*.mp4"))
else:
paths = [path]
for video_path in paths:
try:
video_track = next(iter(MediaInfo.parse(video_path).video_tracks or []))
except StopIteration:
raise click.ClickException("There's no video tracks in the provided file.")
metadata_key = {
"HEVC": "hevc_metadata",
"AVC": "h264_metadata"
}.get(video_track.commercial_name)
if not metadata_key:
raise click.ClickException(f"{video_track.commercial_name} Codec not supported.")
if preview:
out_path = ["-f", "mpegts", "-"] # pipe
else:
out_path = [str(video_path.with_stem(".".join(filter(bool, [
video_path.stem,
video_track.language,
"range",
["limited", "full"][full]
]))).with_suffix({
# ffmpeg's MKV muxer does not yet support HDR
"HEVC": ".h265",
"AVC": ".h264"
}.get(video_track.commercial_name, ".mp4")))]
ffmpeg_call = subprocess.Popen([
executable, "-y",
"-i", str(video_path),
"-map", "0:v:0",
"-c", "copy",
"-bsf:v", f"{metadata_key}=video_full_range_flag={int(full)}"
] + out_path, stdout=subprocess.PIPE)
try:
if preview:
previewer = get_binary_path("mpv", "ffplay")
if not previewer:
raise click.ClickException("MPV/FFplay executables weren't found but are required for previewing.")
subprocess.Popen((previewer, "-"), stdin=ffmpeg_call.stdout)
finally:
if ffmpeg_call.stdout:
ffmpeg_call.stdout.close()
ffmpeg_call.wait()

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import base64
import subprocess
import sys
from typing import Any, Optional, Union, Callable
from typing import Any, Callable, Optional, Union
from uuid import UUID
import m3u8
@ -199,7 +199,7 @@ class Widevine:
for i, (kid, key) in enumerate(self.content_keys.items())
],
*[
# Apple TV+ needs this as their files do not use the KID supplied in it's manifest
# some services use a blank KID on the file, but real KID for license server
"label={}:key_id={}:key={}".format(i, "00" * 16, key.lower())
for i, (kid, key) in enumerate(self.content_keys.items(), len(self.content_keys))
]

View File

@ -240,7 +240,7 @@ class Track:
with open(save_path, "wb") as f:
for file in sorted(segments_dir.iterdir()):
data = file.read_bytes()
# Apple TV+ needs this done to fix audio decryption
# fix audio decryption
data = re.sub(b"(tfhd\x00\x02\x00\x1a\x00\x00\x00\x01\x00\x00\x00)\x02", b"\\g<1>\x01", data)
f.write(data)
file.unlink() # delete, we don't need it anymore