Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions src/livepeer_gateway/media_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ def _normalize_fps(fps: Optional[float]) -> int:
return max(1, int(round(fps)))


# Low-latency encoder options for the trickle output path. x264/x265 keep the
# original superfast/zerolatency set (backward compatible); other codecs start
# from empty defaults so they open cleanly -- the previously hardcoded x264-only
# options (preset=superfast, tune=zerolatency, forced-idr) made non-x264 encoders
# (libsvtav1, libvpx-vp9, *_nvenc, ...) fail at avcodec_open2. Callers add or
# override any option via VideoOutputConfig.encoder_options.
_X264_LOWLATENCY = {"bf": "0", "preset": "superfast", "tune": "zerolatency", "forced-idr": "1"}


def _encoder_base_options(codec: str) -> dict:
if codec in ("libx264", "libx265"):
return dict(_X264_LOWLATENCY)
return {}


@dataclass(frozen=True)
class VideoOutputConfig:
"""
Expand All @@ -76,6 +91,14 @@ class VideoOutputConfig:
codec: str = "libx264"
# Output pixel format presented to the encoder.
pix_fmt: str = "yuv420p"
# Target average video bitrate in bits/sec. None = encoder default (as before).
bit_rate: Optional[int] = None
# H.264/H.265 profile (e.g. "high", "main", "baseline"); codecs without such a
# profile ignore it. None = encoder default.
profile: Optional[str] = None
# Extra ffmpeg encoder options merged over the per-codec defaults, e.g.
# {"preset": "p4", "tune": "ll"} for av1_nvenc. Lets callers tune any encoder.
encoder_options: Optional[dict] = None


@dataclass(frozen=True)
Expand Down Expand Up @@ -657,12 +680,11 @@ def custom_io_open(url: str, flags: int, options: dict) -> object:
assert isinstance(config, VideoOutputConfig)
if not isinstance(track._first_frame, av.VideoFrame):
continue
video_opts = {
"bf": "0",
"preset": "superfast",
"tune": "zerolatency",
"forced-idr": "1",
}
video_opts = _encoder_base_options(config.codec)
if config.profile:
video_opts["profile"] = config.profile
if config.encoder_options:
video_opts.update({str(k): str(v) for k, v in config.encoder_options.items()})
video_kwargs = {
"time_base": _OUT_TIME_BASE,
"width": track._first_frame.width,
Expand All @@ -676,6 +698,11 @@ def custom_io_open(url: str, flags: int, options: dict) -> object:
options=video_opts,
**video_kwargs,
)
if config.bit_rate:
try:
track._stream.codec_context.bit_rate = int(config.bit_rate)
except Exception: # pragma: no cover - encoder may reject
pass
continue

config = track.config
Expand Down
49 changes: 49 additions & 0 deletions tests/test_media_publish_encoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Unit tests for the codec-aware encoder options + VideoOutputConfig fields.

Pure logic — no ffmpeg/trickle needed:
- x264/x265 keep the original low-latency option set (backward compatible).
- other codecs start from empty base options so they open cleanly (the old
hardcoded x264-only options broke non-x264 encoders at avcodec_open2).
- the new bit_rate / profile / encoder_options fields default to "no change".
"""
from __future__ import annotations

from livepeer_gateway.media_publish import (
VideoOutputConfig,
_encoder_base_options,
_X264_LOWLATENCY,
)


def test_x264_family_keeps_lowlatency_defaults():
assert _encoder_base_options("libx264") == _X264_LOWLATENCY
assert _encoder_base_options("libx265") == _X264_LOWLATENCY
# returns a copy, not the shared dict (callers mutate it)
opts = _encoder_base_options("libx264")
opts["preset"] = "veryfast"
assert _X264_LOWLATENCY["preset"] == "superfast"


def test_non_x264_codecs_get_empty_base_options():
for codec in ("libsvtav1", "av1_nvenc", "libvpx-vp9", "h264_nvenc", "hevc_nvenc"):
assert _encoder_base_options(codec) == {}


def test_video_output_config_defaults_are_backward_compatible():
cfg = VideoOutputConfig()
assert cfg.bit_rate is None
assert cfg.profile is None
assert cfg.encoder_options is None
assert cfg.codec == "libx264"


def test_video_output_config_accepts_new_fields():
cfg = VideoOutputConfig(
codec="av1_nvenc",
bit_rate=3_000_000,
profile="high",
encoder_options={"preset": "p4", "tune": "ll"},
)
assert cfg.bit_rate == 3_000_000
assert cfg.profile == "high"
assert cfg.encoder_options == {"preset": "p4", "tune": "ll"}