mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-14 21:15:06 +00:00
feat(dash): add merge_segments to append segments during the download
Opt-in rolling merge for DASH video and audio: each contiguous segment is appended to the output as it lands, so the post-download "Merging" stage goes away. Decrypts the init segment eagerly when decrypt_segments is on, disables resume for the track, and skips subtitle tracks.
This commit is contained in:
@@ -24,6 +24,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
import pytest
|
||||
|
||||
from unshackle.core.config import config
|
||||
from unshackle.core.constants import DOWNLOAD_CANCELLED, DownloadCancelled
|
||||
from unshackle.core.manifests import DASH
|
||||
from unshackle.core.tracks.track import DownloadContext
|
||||
@@ -235,3 +236,33 @@ def test_cancelled_sibling_does_not_report_missing_segments(server, tmp_path):
|
||||
# missing segments here would mask the sibling failure that set the flag
|
||||
assert track.path is None
|
||||
assert not ctx.save_path.exists()
|
||||
|
||||
|
||||
def test_rolling_merge_appends_in_order_and_leaves_no_segments(server, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(config, "merge_segments", True)
|
||||
server.routes["/init.bin"] = INIT2
|
||||
server.routes["/part0.bin"] = PART0
|
||||
server.routes["/part1.bin"] = PART1
|
||||
track = make_track(mixed_mpd(server), server)
|
||||
ctx = make_ctx(tmp_path, "rolling.mp4")
|
||||
|
||||
DASH.download_track(track, ctx)
|
||||
|
||||
assert ctx.save_path.read_bytes() == INIT2 + PART0 + PART1
|
||||
assert not ctx.save_dir.exists() or not list(ctx.save_dir.glob("*"))
|
||||
|
||||
|
||||
def test_rolling_merge_cancel_leaves_no_partial_output(server, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(config, "merge_segments", True)
|
||||
server.routes["/init.bin"] = INIT2
|
||||
server.routes["/part0.bin"] = PART0
|
||||
server.routes["/part1.bin"] = PART1
|
||||
track = make_track(mixed_mpd(server), server)
|
||||
ctx = make_ctx(tmp_path, "rolling-cancelled.mp4")
|
||||
|
||||
DOWNLOAD_CANCELLED.set()
|
||||
with pytest.raises(DownloadCancelled):
|
||||
DASH.download_track(track, ctx)
|
||||
|
||||
assert track.path is None
|
||||
assert not ctx.save_path.exists()
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import io
|
||||
import random
|
||||
from concurrent.futures import Future
|
||||
from pathlib import Path
|
||||
|
||||
from unshackle.core.manifests.dash import RollingMerge, append_segment
|
||||
|
||||
|
||||
def write_segments(save_dir: Path, payloads: list[bytes]) -> int:
|
||||
save_dir.mkdir()
|
||||
width = len(str(len(payloads)))
|
||||
for i, payload in enumerate(payloads):
|
||||
(save_dir / f"{i:0{width}d}.mp4").write_bytes(payload)
|
||||
return width
|
||||
|
||||
|
||||
def expected(init: bytes, payloads: list[bytes], is_text_subtitle: bool, tmp_path: Path) -> bytes:
|
||||
scratch = tmp_path / "scratch"
|
||||
width = write_segments(scratch, payloads)
|
||||
out = io.BytesIO()
|
||||
out.write(init)
|
||||
for i in range(len(payloads)):
|
||||
append_segment(out, scratch / f"{i:0{width}d}.mp4", is_text_subtitle)
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def test_out_of_order_segments_match_whole_merge(tmp_path: Path) -> None:
|
||||
rng = random.Random(7)
|
||||
payloads = [rng.randbytes(rng.randint(1, 4096)) for _ in range(23)]
|
||||
init = b"ftyp-moov"
|
||||
want = expected(init, payloads, False, tmp_path)
|
||||
|
||||
save_dir = tmp_path / "segments"
|
||||
width = write_segments(save_dir, payloads)
|
||||
dst = io.BytesIO()
|
||||
dst.write(init)
|
||||
merger = RollingMerge(dst, save_dir, width)
|
||||
|
||||
order = list(range(len(payloads)))
|
||||
rng.shuffle(order)
|
||||
for index in order:
|
||||
future: Future = Future()
|
||||
future.set_result(None)
|
||||
merger.add(index, future if index % 2 else None)
|
||||
# the cursor never rests on a segment that is ready: every contiguous run is drained
|
||||
assert merger.merged not in merger.ready
|
||||
|
||||
assert merger.merged == len(payloads)
|
||||
assert dst.getvalue() == want
|
||||
assert dst.getvalue().startswith(init)
|
||||
assert list(save_dir.iterdir()) == []
|
||||
Reference in New Issue
Block a user