A share link is opened on whatever the friend holds. Measured on an iPhone and an iPad: AV1, Matroska, HEVC tagged hev1 and MP3 inside an .mp4 fail there; Chrome and Firefox add HEVC and 10-bit / 4:4:4 H.264. The old check judged the codec NAME only and ran on downloads only, so a 10-bit H.264 passed, an upload went out exactly as it came (an iPhone's HEVC .mov, a webm, an mkv) and a fast trim kept its source's codecs. One rule now, `downloads/playable.py`: video is H.264 8-bit 4:2:0 + AAC in .mp4, audio-only is .mp3 or AAC .m4a; `plan` says what must change, `build_cmd` re-encodes only the failing stream (or remuxes). The worker applies it to a download, an editor clip and — new — an upload: media that fails the rule is queued (asset pending, job queued) and the worker converts it, keeping only the converted file (owner's choice). Until then the original sits at rel_path, so Retry starts from it and a delete removes it; an upload that lost its last holder mid-conversion is reaped by the GC. Every libx264 run (edit, concat, conversion) now pins `-pix_fmt yuv420p` — without it a 10-bit source came out High 10. The download cache signature moves to v5 so a cached 10-bit asset re-derives. The upload toast says when the file is still converting (HU/EN). Tests: the plan for every measured class; real ffmpeg turning a 10-bit Matroska + Opus and an mp3-in-mp4 into files the rule accepts (red without the pixel-format pin); the queued upload's delete, cancel-then-GC, failed conversion and real worker conversion (the cleanup tests red without their rules); the toast (red without the branch). Live on localdev: 9 fixture uploads converted, every served file probed H.264 High yuv420p + AAC, or mp3 / m4a.
104 lines
4.4 KiB
Python
104 lines
4.4 KiB
Python
"""Worker-child resource de-prioritization: the encoder thread cap, the `-threads` flag on the
|
|
browser-safe re-encode, and the child's startup renice/ionice/OMP handicap. These keep a CPU-heavy
|
|
transcode from pegging every core on the box it shares with the API, Postgres, and Plex playback."""
|
|
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
from app.config import settings
|
|
from app.downloads import edit as editmod
|
|
from app.downloads import playable
|
|
from app.downloads import runner
|
|
|
|
|
|
class TestEncodeThreadCap:
|
|
def test_pinned_setting_wins(self, monkeypatch):
|
|
monkeypatch.setattr(settings, "download_ffmpeg_threads", 3)
|
|
assert editmod.encode_thread_cap() == 3
|
|
|
|
def test_zero_reserves_one_core(self, monkeypatch):
|
|
monkeypatch.setattr(settings, "download_ffmpeg_threads", 0)
|
|
monkeypatch.setattr(editmod.os, "cpu_count", lambda: 8)
|
|
assert editmod.encode_thread_cap() == 7
|
|
|
|
def test_single_core_box_never_caps_below_one(self, monkeypatch):
|
|
monkeypatch.setattr(settings, "download_ffmpeg_threads", 0)
|
|
monkeypatch.setattr(editmod.os, "cpu_count", lambda: 1)
|
|
assert editmod.encode_thread_cap() == 1
|
|
|
|
def test_unknown_cpu_count_falls_back(self, monkeypatch):
|
|
monkeypatch.setattr(settings, "download_ffmpeg_threads", 0)
|
|
monkeypatch.setattr(editmod.os, "cpu_count", lambda: None)
|
|
assert editmod.encode_thread_cap() == 1 # max(1, 2 - 1)
|
|
|
|
|
|
_REENCODE_VIDEO = playable.Plan("mp4", True, True, False, True)
|
|
|
|
|
|
class TestPhoneSafeThreads:
|
|
def test_threads_flag_added_when_capped(self):
|
|
cmd = playable.build_cmd("/in.mkv", "/out.mp4", _REENCODE_VIDEO, threads=2)
|
|
assert "-threads" in cmd
|
|
assert cmd[cmd.index("-threads") + 1] == "2"
|
|
# The cap precedes the output file, so it binds the encoder (not input decoding).
|
|
assert cmd.index("-threads") < cmd.index("/out.mp4")
|
|
|
|
@pytest.mark.parametrize("threads", [0, None])
|
|
def test_no_threads_flag_when_uncapped(self, threads):
|
|
cmd = playable.build_cmd("/in.mkv", "/out.mp4", _REENCODE_VIDEO, threads=threads)
|
|
assert "-threads" not in cmd
|
|
|
|
|
|
class TestDeprioritize:
|
|
def test_renices_by_configured_increment(self, monkeypatch):
|
|
monkeypatch.setattr(settings, "download_child_nice", 12)
|
|
monkeypatch.setattr(settings, "download_child_ionice_class", 0) # skip ionice branch
|
|
calls = []
|
|
monkeypatch.setattr(runner.os, "nice", lambda n: calls.append(n))
|
|
monkeypatch.setattr(runner.os, "environ", {})
|
|
runner._deprioritize()
|
|
assert calls == [12]
|
|
|
|
def test_nice_zero_does_not_renice(self, monkeypatch):
|
|
monkeypatch.setattr(settings, "download_child_nice", 0)
|
|
monkeypatch.setattr(settings, "download_child_ionice_class", 0)
|
|
calls = []
|
|
monkeypatch.setattr(runner.os, "nice", lambda n: calls.append(n))
|
|
monkeypatch.setattr(runner.os, "environ", {})
|
|
runner._deprioritize()
|
|
assert calls == []
|
|
|
|
def test_sets_omp_budget_to_thread_cap(self, monkeypatch):
|
|
monkeypatch.setattr(settings, "download_child_nice", 0)
|
|
monkeypatch.setattr(settings, "download_child_ionice_class", 0)
|
|
monkeypatch.setattr(settings, "download_ffmpeg_threads", 4)
|
|
env: dict = {}
|
|
monkeypatch.setattr(runner.os, "environ", env)
|
|
runner._deprioritize()
|
|
assert env["OMP_NUM_THREADS"] == "4"
|
|
|
|
def test_ionice_invoked_when_binary_present(self, monkeypatch):
|
|
monkeypatch.setattr(settings, "download_child_nice", 0)
|
|
monkeypatch.setattr(settings, "download_child_ionice_class", 3)
|
|
monkeypatch.setattr(runner.os, "environ", {})
|
|
monkeypatch.setattr(runner.shutil, "which", lambda name: "/usr/bin/ionice")
|
|
ran = []
|
|
monkeypatch.setattr(
|
|
runner.subprocess,
|
|
"run",
|
|
lambda cmd, **kw: ran.append(cmd) or subprocess.CompletedProcess(cmd, 0),
|
|
)
|
|
runner._deprioritize()
|
|
assert ran and ran[0][:3] == ["ionice", "-c", "3"]
|
|
|
|
def test_ionice_skipped_when_binary_absent(self, monkeypatch):
|
|
monkeypatch.setattr(settings, "download_child_nice", 0)
|
|
monkeypatch.setattr(settings, "download_child_ionice_class", 3)
|
|
monkeypatch.setattr(runner.os, "environ", {})
|
|
monkeypatch.setattr(runner.shutil, "which", lambda name: None)
|
|
ran = []
|
|
monkeypatch.setattr(runner.subprocess, "run", lambda cmd, **kw: ran.append(cmd))
|
|
runner._deprioritize()
|
|
assert ran == []
|