OpenTranscode/tests/test_theora_ogv.py

277 lines
13 KiB
Python

"""Theora/OGV target tests — v4.9.0.
Covers the three pieces of the OGV scenario:
1. The quality-scale wrapper (``theora_quality_from_crf``) — libtheora
grades quality with ``-q:v 0..31`` where HIGHER is better, the exact
opposite of the shared CRF knob. The wrapper must invert linearly,
clamp its input, and stay inside ``THEORA_Q_RANGE``.
2. The ``Theora (OGV)`` VideoCodecProfile + ``OGV (Ogg)`` container —
ffmpeg-only flag, no GPU family, ffprobe codec name for
skip-existing, and encoder args that use ``-q:v`` (never ``-crf``).
3. The UI compatibility rules — Theora is INCOMPATIBLE with MP4/WebM,
valid-but-unusual in MKV (WARNING), the OGV container is
INCOMPATIBLE with every other video codec, and IAMF audio stays
INCOMPATIBLE in OGV via the existing non-MP4 rule.
Empirical grounding (ffmpeg 7.x, libtheora): bitrate rises monotonically
with -q:v and saturates from ~31; the mp4 muxer rejects Theora; the webm
muxer only accepts VP8/VP9/AV1; the ogg muxer only accepts Theora video;
Opus, Vorbis and FLAC all mux fine into Ogg.
No PySide6 required: profile/wrapper tests import ``codec_profiles``
directly (pure data + pure functions); the compat-rule tests reuse the
``__new__``-mock pattern from test_container_compat.py, which works with
the conftest PySide6 stubs.
"""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from conftest import load_program_module
_m = load_program_module()
CONTAINER_PROFILES = _m.CONTAINER_PROFILES
FFMPEG_LIB_KEY_MAP = _m.FFMPEG_LIB_KEY_MAP
THEORA_Q_RANGE = _m.THEORA_Q_RANGE
VIDEO_CODECS = _m.VIDEO_CODECS
theora_quality_from_crf = _m.theora_quality_from_crf
# ─────────────────────────────────────────────────────────────────────────────
# Lookups — the Theora profile and OGV container, found by VALUE so the
# tests don't depend on list positions (the combos index into the same
# lists, but the tables are free to grow).
# ─────────────────────────────────────────────────────────────────────────────
def _theora_profile():
matches = [vc for vc in VIDEO_CODECS if vc.ffmpeg_encoder == "libtheora"]
assert matches, "VIDEO_CODECS must contain a Theora profile"
return matches[0]
def _container_exts():
return {cp.ext for cp in CONTAINER_PROFILES}
# ─────────────────────────────────────────────────────────────────────────────
# 1. The quality-scale wrapper
# ─────────────────────────────────────────────────────────────────────────────
class TestTheoraQualityWrapper:
def test_best_knob_position_maps_to_best_q(self):
# Knob 18 = the knob's best → q 31 = libtheora's best (inverted!)
assert theora_quality_from_crf(18) == THEORA_Q_RANGE[1] == 31
def test_worst_knob_position_maps_to_worst_q(self):
assert theora_quality_from_crf(52) == THEORA_Q_RANGE[0] == 0
def test_default_knob_position(self):
# The 4.8.2 default knob value (26) lands at q 24 — high-quality
# archival territory on the 0..31 scale.
assert theora_quality_from_crf(26) == 24
def test_monotonic_non_increasing_over_all_integers(self):
# Turning the knob toward WORSE quality must never RAISE q
# (per-integer steps can repeat a value: 34 knob integers map
# onto 32 q values).
values = [theora_quality_from_crf(crf) for crf in range(18, 53)]
assert all(a >= b for a, b in zip(values, values[1:])), values
def test_strictly_decreasing_per_knob_tick(self):
# The knob snaps to every-2 steps (snap_ticks over 18..52); each
# full user-visible tick must strictly drop q (~1.8 per tick).
ticks = [theora_quality_from_crf(crf) for crf in range(18, 53, 2)]
assert all(a > b for a, b in zip(ticks, ticks[1:])), ticks
def test_output_stays_inside_q_scale(self):
for crf in range(-10, 80): # far outside the knob range
q = theora_quality_from_crf(crf)
assert THEORA_Q_RANGE[0] <= q <= THEORA_Q_RANGE[1], (crf, q)
def test_custom_knob_range_is_respected(self):
# The wrapper takes the range as parameters so a future profile
# with a different crf_range can reuse it.
assert theora_quality_from_crf(10, crf_lo=10, crf_hi=20) == 31
assert theora_quality_from_crf(20, crf_lo=10, crf_hi=20) == 0
# ─────────────────────────────────────────────────────────────────────────────
# 2. Profile / container invariants
# ─────────────────────────────────────────────────────────────────────────────
class TestTheoraProfile:
def test_profile_is_ffmpeg_only(self):
vc = _theora_profile()
assert vc.ffmpeg_only is True
assert vc.av1an_encoder == ""
def test_params_fn_raises_loudly(self):
# The av1an path can never legitimately build Theora params; the
# guard turns a would-be opaque av1an CLI failure into a clear
# RuntimeError naming the fix.
with pytest.raises(RuntimeError, match="ffmpeg_only"):
_theora_profile().params_fn(26, 0)
def test_ffmpeg_args_use_qscale_not_crf(self):
args = _theora_profile().ffmpeg_vargs_fn(26, 1)
assert args[0:2] == ["-c:v", "libtheora"]
assert "-q:v" in args
q_pos = args.index("-q:v")
assert args[q_pos + 1] == "24" # 4.8.2 default → q 24
assert "-crf" not in args # libtheora has no CRF
assert "-pix_fmt" in args
assert args[args.index("-pix_fmt") + 1] == "yuv420p" # 4:2:0-only codec
def test_preset_maps_to_speed_level_and_clamps(self):
vc = _theora_profile()
for label, level in vc.preset_map.items():
args = vc.ffmpeg_vargs_fn(26, level)
assert args[args.index("-speed_level") + 1] == str(level)
# Out-of-range preset values (e.g. the EncoderWorker fallback 6)
# clamp to libtheora's maximum speed level 2 instead of erroring.
clamped = vc.ffmpeg_vargs_fn(26, 6)
assert clamped[clamped.index("-speed_level") + 1] == "2"
def test_skip_existing_codec_name(self):
assert _theora_profile().ffprobe_codec_name == "theora"
def test_no_gpu_family(self):
# No hardware Theora encoder exists on any API — resolve_gpu_
# encoder() must land on CPU for every engine selection.
assert _theora_profile().gpu_family == ""
assert _theora_profile().gpu_encoder == ""
def test_ogv_container_exists_and_is_default(self):
vc = _theora_profile()
assert vc.container == "ogv"
assert "ogv" in _container_exts()
def test_lib_key_map_covers_theora(self):
# _disable_unavailable_codecs and the ffmpeg-fallback gate both
# look the encoder up through FFMPEG_LIB_KEY_MAP — a missing
# entry would silently report the codec as unavailable forever.
assert FFMPEG_LIB_KEY_MAP["libtheora"] == "libtheora"
# ─────────────────────────────────────────────────────────────────────────────
# 3. UI compatibility rules (same __new__-mock pattern as
# test_container_compat.py — works with or without real PySide6)
# ─────────────────────────────────────────────────────────────────────────────
def _make_master_for_compat(codec_idx, audio_idx, container_idx):
OpenCodecMaster = load_program_module().OpenCodecMaster
master = OpenCodecMaster.__new__(OpenCodecMaster)
codec_combo = MagicMock()
codec_combo.currentIndex.return_value = codec_idx
audio_combo = MagicMock()
audio_combo.currentIndex.return_value = audio_idx
container_combo = MagicMock()
container_combo.currentIndex.return_value = container_idx
master.codec_combo = codec_combo
master.audio_combo = audio_combo
master.container_combo = container_combo
logged: list[str] = []
master._log = lambda msg: logged.append(msg)
return master, logged
def _codec_idx(encoder: str) -> int:
for i, vc in enumerate(VIDEO_CODECS):
if vc.ffmpeg_encoder == encoder:
return i
raise AssertionError(f"no profile with ffmpeg_encoder={encoder!r}")
def _container_idx(ext: str) -> int:
for i, cp in enumerate(CONTAINER_PROFILES):
if cp.ext == ext:
return i
raise AssertionError(f"no container with ext={ext!r}")
def _audio_idx(encoder: str) -> int:
AUDIO_PROFILES = load_program_module().AUDIO_PROFILES
for i, ap in enumerate(AUDIO_PROFILES):
if ap.ffmpeg_encoder_name == encoder:
return i
raise AssertionError(f"no audio profile {encoder!r}")
class TestTheoraCompatRules:
def test_theora_in_webm_incompatible(self):
master, _ = _make_master_for_compat(
_codec_idx("libtheora"), _audio_idx("libopus"),
_container_idx("webm"),
)
warnings = master._check_combo_compatibility()
assert any(w.startswith("INCOMPATIBLE:") and "Theora" in w
for w in warnings), warnings
def test_theora_in_mp4_incompatible(self):
master, _ = _make_master_for_compat(
_codec_idx("libtheora"), _audio_idx("libopus"),
_container_idx("mp4"),
)
warnings = master._check_combo_compatibility()
assert any(w.startswith("INCOMPATIBLE:") and "Theora" in w
for w in warnings), warnings
def test_theora_in_mkv_is_warning_not_block(self):
# MKV really can hold Theora (verified with a live encode), so it
# must NOT hard-block — just advise OGV as the canonical pairing.
master, _ = _make_master_for_compat(
_codec_idx("libtheora"), _audio_idx("libvorbis"),
_container_idx("mkv"),
)
warnings = master._check_combo_compatibility()
assert any(w.startswith("WARNING:") and "Theora" in w
for w in warnings), warnings
assert not any(w.startswith("INCOMPATIBLE:") for w in warnings)
def test_theora_in_ogv_is_clean(self):
# The canonical pairing (with Ogg-native audio) fires no rule.
master, _ = _make_master_for_compat(
_codec_idx("libtheora"), _audio_idx("libvorbis"),
_container_idx("ogv"),
)
assert master._check_combo_compatibility() == []
def test_theora_flac_opus_in_ogv_are_clean(self):
# Vorbis, Opus and FLAC are all Ogg-standard audio codecs.
for audio in ("libopus", "flac"):
master, _ = _make_master_for_compat(
_codec_idx("libtheora"), _audio_idx(audio),
_container_idx("ogv"),
)
assert master._check_combo_compatibility() == [], audio
@pytest.mark.parametrize("encoder", ["libsvtav1", "libvpx-vp9", "libx265"])
def test_non_theora_in_ogv_incompatible(self, encoder):
# The Ogg muxer rejects every other video codec — the user must
# switch to Theora or a different container.
master, _ = _make_master_for_compat(
_codec_idx(encoder), _audio_idx("libopus"),
_container_idx("ogv"),
)
warnings = master._check_combo_compatibility()
assert any(w.startswith("INCOMPATIBLE:") and "Ogg" in w
for w in warnings), (encoder, warnings)
def test_iamf_in_ogv_incompatible_via_existing_rule(self):
# IAMF requires MP4; the pre-existing non-MP4 rule covers OGV
# with no Theora-specific additions.
master, _ = _make_master_for_compat(
_codec_idx("libtheora"), _audio_idx("libiamf"),
_container_idx("ogv"),
)
warnings = master._check_combo_compatibility()
assert any(w.startswith("INCOMPATIBLE:") and "IAMF" in w
for w in warnings), warnings