mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
fix(mux): strip encoding dates from MP4Box and shaka-packager output
MP4Box gets -for-test (non-MP4 inputs keep their tkhd flags) and shaka decrypts get --use_fake_clock_for_muxer, so MP4 and --no-mux files carry no download time.
This commit is contained in:
@@ -482,11 +482,11 @@ Set `default_subtitle_type` to give the subtitle default to a different type. `d
|
|||||||
|
|
||||||
A few muxing behaviors are automatic and not configurable:
|
A few muxing behaviors are automatic and not configurable:
|
||||||
|
|
||||||
- `--no-date` is always passed for privacy (no timestamps embedded).
|
- The output has no encoding date: mkvmerge gets `--no-date` and MP4Box gets `-for-test`.
|
||||||
- Descriptive audio gets the visually-impaired flag. SDH subtitles get the hearing-impaired flag. Forced subtitles get the forced flag.
|
- Descriptive audio gets the visually-impaired flag. SDH subtitles get the hearing-impaired flag. Forced subtitles get the forced flag.
|
||||||
- unshackle writes the chapters from the title's chapter list (see [Chapters](#chapters-naming) below).
|
- unshackle writes the chapters from the title's chapter list (see [Chapters](#chapters-naming) below).
|
||||||
|
|
||||||
The `--no-mux` flag skips muxing entirely and writes the individual track files, each with a track-type suffix, into the same output/folder structure.
|
The `--no-mux` flag skips muxing entirely and writes the individual track files, each with a track-type suffix, into the same output/folder structure. A track that shaka-packager decrypts has its date set to 1970-01-01; other tracks keep the date of the source file.
|
||||||
|
|
||||||
## Sample files
|
## Sample files
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ def test_dts_uhd_audio_routes_the_mux_to_mp4(tmp_path: Path, monkeypatch: pytest
|
|||||||
assert out.suffix == ".mp4", "a DTS-UHD title must not be delivered as Matroska"
|
assert out.suffix == ".mp4", "a DTS-UHD title must not be delivered as Matroska"
|
||||||
assert returncode == 0 and not errors
|
assert returncode == 0 and not errors
|
||||||
command = captured[0].args
|
command = captured[0].args
|
||||||
assert command[0] == "MP4Box"
|
assert command[:2] == ["MP4Box", "-for-test"]
|
||||||
assert command[-2:] == ["-new", str(out)]
|
assert command[-2:] == ["-new", str(out)]
|
||||||
assert f"{tmp_path / 'a.mp4'}:lang=en" in command
|
assert f"{tmp_path / 'a.mp4'}:lang=en" in command
|
||||||
# the child must not inherit the terminal that the live progress tree is drawing on
|
# the child must not inherit the terminal that the live progress tree is drawing on
|
||||||
@@ -170,7 +170,7 @@ def test_mp4_subtitles_get_the_script_their_region_implies(tmp_path: Path, monke
|
|||||||
|
|
||||||
tracks.mux("Title", delete=False, output_path=tmp_path / "out.muxed.mkv")
|
tracks.mux("Title", delete=False, output_path=tmp_path / "out.muxed.mkv")
|
||||||
command = captured[0].args
|
command = captured[0].args
|
||||||
assert f"{tmp_path / 's.vtt'}:lang=zh-Hant-TW" in command
|
assert f"{tmp_path / 's.vtt'}:lang=zh-Hant-TW:tkhd=enable,movie,preview" in command
|
||||||
assert any(arg.endswith("=Chinese (Traditional)") for arg in command)
|
assert any(arg.endswith("=Chinese (Traditional)") for arg in command)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,12 @@ def mp4box_commands(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> list[lis
|
|||||||
|
|
||||||
monkeypatch.setattr(binaries, "MP4Box", Path("/bin/MP4Box"), raising=False)
|
monkeypatch.setattr(binaries, "MP4Box", Path("/bin/MP4Box"), raising=False)
|
||||||
monkeypatch.setattr(tracks_module.subprocess, "Popen", FakePopen)
|
monkeypatch.setattr(tracks_module.subprocess, "Popen", FakePopen)
|
||||||
monkeypatch.setattr(tracks_module, "run_step", lambda command, **kwargs: commands.append([str(c) for c in command]))
|
|
||||||
|
def run_step(command: list[Any], output: Path, **kwargs: Any) -> None:
|
||||||
|
commands.append([str(c) for c in command])
|
||||||
|
output.write_bytes(b"cut")
|
||||||
|
|
||||||
|
monkeypatch.setattr(tracks_module, "run_step", run_step)
|
||||||
monkeypatch.setattr(config, "sample", None)
|
monkeypatch.setattr(config, "sample", None)
|
||||||
monkeypatch.setattr(config.directories, "temp", tmp_path)
|
monkeypatch.setattr(config.directories, "temp", tmp_path)
|
||||||
return commands
|
return commands
|
||||||
@@ -136,6 +141,15 @@ def test_a_3d_mp4_mux_keeps_the_sample_whole(
|
|||||||
Tracks(stereo_video(tmp_path))._mux_mp4(title="Film", output_path=tmp_path / "out.mkv")
|
Tracks(stereo_video(tmp_path))._mux_mp4(title="Film", output_path=tmp_path / "out.mkv")
|
||||||
assert len(mp4box_commands) == 1
|
assert len(mp4box_commands) == 1
|
||||||
assert "-splitx" not in mp4box_commands[0]
|
assert "-splitx" not in mp4box_commands[0]
|
||||||
|
assert "-for-test" in mp4box_commands[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_2d_mp4_sample_cut_keeps_no_date(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mp4box_commands: list[list[str]]
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(config, "sample", (120.0, 180.0))
|
||||||
|
Tracks(stereo_video(tmp_path, stereo=False))._mux_mp4(title="Film", output_path=tmp_path / "out.mkv")
|
||||||
|
assert mp4box_commands[1][1:3] == ["-for-test", "-splitx"]
|
||||||
|
|
||||||
|
|
||||||
def test_a_3d_mp4_mux_writes_quicktime_chapters(tmp_path: Path, mp4box_commands: list[list[str]]) -> None:
|
def test_a_3d_mp4_mux_writes_quicktime_chapters(tmp_path: Path, mp4box_commands: list[list[str]]) -> None:
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ def test_shaka_decrypt_passes_the_built_keys(monkeypatch: pytest.MonkeyPatch, tm
|
|||||||
|
|
||||||
keys = seen[0][seen[0].index("--keys") + 1]
|
keys = seen[0][seen[0].index("--keys") + 1]
|
||||||
assert _shaka_zero_entries(keys) == [VIDEO_KEY]
|
assert _shaka_zero_entries(keys) == [VIDEO_KEY]
|
||||||
|
assert "--use_fake_clock_for_muxer" in seen[0]
|
||||||
|
|
||||||
|
|
||||||
def test_a_rebuilt_widevine_does_not_take_the_first_pssh_kid_as_its_own() -> None:
|
def test_a_rebuilt_widevine_does_not_take_the_first_pssh_kid_as_its_own() -> None:
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ class ClearKeyCENC:
|
|||||||
"label={}:key_id={}:key={}".format(i, kid.hex, key.lower())
|
"label={}:key_id={}:key={}".format(i, kid.hex, key.lower())
|
||||||
for i, (kid, key) in enumerate(self.content_keys.items())
|
for i, (kid, key) in enumerate(self.content_keys.items())
|
||||||
),
|
),
|
||||||
|
"--use_fake_clock_for_muxer",
|
||||||
"--temp_dir",
|
"--temp_dir",
|
||||||
config.directories.temp,
|
config.directories.temp,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -654,6 +654,7 @@ class PlayReady:
|
|||||||
"--enable_raw_key_decryption",
|
"--enable_raw_key_decryption",
|
||||||
"--keys",
|
"--keys",
|
||||||
drm_key_args.shaka_keys(self.content_keys, self.own_kids()),
|
drm_key_args.shaka_keys(self.content_keys, self.own_kids()),
|
||||||
|
"--use_fake_clock_for_muxer",
|
||||||
"--temp_dir",
|
"--temp_dir",
|
||||||
config.directories.temp,
|
config.directories.temp,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -404,6 +404,7 @@ class Widevine:
|
|||||||
"--enable_raw_key_decryption",
|
"--enable_raw_key_decryption",
|
||||||
"--keys",
|
"--keys",
|
||||||
drm_key_args.shaka_keys(self.content_keys, self.own_kids()),
|
drm_key_args.shaka_keys(self.content_keys, self.own_kids()),
|
||||||
|
"--use_fake_clock_for_muxer",
|
||||||
"--temp_dir",
|
"--temp_dir",
|
||||||
config.directories.temp,
|
config.directories.temp,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -69,10 +69,9 @@ def file_track_ids(path: Path) -> dict[str, int]:
|
|||||||
return ids or {"video": 0, "audio": 1}
|
return ids or {"video": 0, "audio": 1}
|
||||||
|
|
||||||
|
|
||||||
def mp4_track_count(path: Path) -> int:
|
def mp4_track_count(moov: Optional[bytes]) -> int:
|
||||||
"""How many tracks ``MP4Box -add`` takes from a file: each ``trak`` of an MP4, or 1 for any other file."""
|
"""How many tracks ``MP4Box -add`` takes from a file: each ``trak`` of its moov, or 1 for a file with none."""
|
||||||
moov = read_top_level_box(path, b"moov") or b""
|
return sum(1 for kind, _, _ in children(moov or b"", 8) if kind == b"trak") or 1
|
||||||
return sum(1 for kind, _, _ in children(moov, 8) if kind == b"trak") or 1
|
|
||||||
|
|
||||||
|
|
||||||
class Tracks:
|
class Tracks:
|
||||||
@@ -677,7 +676,8 @@ class Tracks:
|
|||||||
(MV-HEVC) video, whose second view mkvmerge drops. MP4 carries far less: it drops the
|
(MV-HEVC) video, whose second view mkvmerge drops. MP4 carries far less: it drops the
|
||||||
forced, hearing-impaired and original flags, holds no attachments at all, and leaves
|
forced, hearing-impaired and original flags, holds no attachments at all, and leaves
|
||||||
out a subtitle it has no format for rather than mangling it. Losing those beats
|
out a subtitle it has no format for rather than mangling it. Losing those beats
|
||||||
shipping audio no player can name or a 3D film as 2D.
|
shipping audio no player can name or a 3D film as 2D. ``-for-test`` drops the dates and
|
||||||
|
the GPAC version tag; ``tkhd`` restores the flags it clears on a non-MP4 input.
|
||||||
|
|
||||||
Returns the same (path, returncode, errors) shape as the Matroska mux.
|
Returns the same (path, returncode, errors) shape as the Matroska mux.
|
||||||
"""
|
"""
|
||||||
@@ -688,7 +688,7 @@ class Tracks:
|
|||||||
progress = progress or partial(lambda **kwargs: None)
|
progress = progress or partial(lambda **kwargs: None)
|
||||||
|
|
||||||
stereo = any(v.stereo for v in self.videos)
|
stereo = any(v.stereo for v in self.videos)
|
||||||
cl = [str(mp4box)]
|
cl = [str(mp4box), "-for-test"]
|
||||||
sample_base = self.sample_base
|
sample_base = self.sample_base
|
||||||
names: list[str] = []
|
names: list[str] = []
|
||||||
cleaned: list[Path] = []
|
cleaned: list[Path] = []
|
||||||
@@ -723,11 +723,13 @@ class Tracks:
|
|||||||
candidate.unlink()
|
candidate.unlink()
|
||||||
|
|
||||||
delay = round((track.sample_start - sample_base) * 1000) if config.sample else 0
|
delay = round((track.sample_start - sample_base) * 1000) if config.sample else 0
|
||||||
cl.extend(["-add", f"{source}:lang={track.mux_language}" + (f":delay={delay}" if delay else "")])
|
moov = read_top_level_box(source, b"moov")
|
||||||
|
flags = "" if moov else ":tkhd=enable,movie,preview"
|
||||||
|
cl.extend(["-add", f"{source}:lang={track.mux_language}{flags}" + (f":delay={delay}" if delay else "")])
|
||||||
name = "" if isinstance(track, Video) else (track.get_track_name() or "")
|
name = "" if isinstance(track, Video) else (track.get_track_name() or "")
|
||||||
if name:
|
if name:
|
||||||
names.extend(["-name", f"{track_id + 1}={name}"])
|
names.extend(["-name", f"{track_id + 1}={name}"])
|
||||||
track_id += mp4_track_count(source)
|
track_id += mp4_track_count(moov)
|
||||||
|
|
||||||
if not track_id:
|
if not track_id:
|
||||||
raise ValueError("No tracks provided, at least one track must be provided.")
|
raise ValueError("No tracks provided, at least one track must be provided.")
|
||||||
@@ -806,7 +808,7 @@ class Tracks:
|
|||||||
cut_path = output_path.with_suffix(".cut.mp4")
|
cut_path = output_path.with_suffix(".cut.mp4")
|
||||||
try:
|
try:
|
||||||
run_step(
|
run_step(
|
||||||
[mp4box, "-splitx", f"{start:.3f}:{end:.3f}", output_path, "-out", cut_path],
|
[mp4box, "-for-test", "-splitx", f"{start:.3f}:{end:.3f}", output_path, "-out", cut_path],
|
||||||
output=cut_path,
|
output=cut_path,
|
||||||
label="MP4Box sample cut",
|
label="MP4Box sample cut",
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user