277 lines
13 KiB
Python
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
|