diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c730db..85a4552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `realtime` flag steers VideoToolbox into the low-latency code path. - Encoder + preset selection moved into `engine.ResolveEncoderProfile` so the same logic drives both argv construction and the log line. +- **`download.transcode.preset` is now libx264-only**. The configured preset + is honoured on software encode (libx264 vocabulary: ultrafast → + veryslow); HW backends ignore it and use vendor-specific defaults + (NVENC p3, QSV veryfast). Passing a libx264 preset to NVENC / QSV was + previously rejected by ffmpeg; the documentation now reflects what was + always the only correct usage. +- Default `download.transcode.preset` is empty (was `"veryfast"`). The + engine fills in `"superfast"` for libx264 — latency-biased. **Users who + want better quality at slower first-play should set it explicitly in + `config.toml`**: `"veryfast"` (previous default) / `"faster"` / `"fast"` + / `"medium"`. Range documented in the TranscodeConfig struct. ## [0.9.8] - 2026-05-27 diff --git a/internal/config/config.go b/internal/config/config.go index dfa5e8a..dd406a6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -96,9 +96,27 @@ type VPNConfig struct { // Disabled by default; enabling requires ffmpeg + ffprobe on PATH (or // explicit paths via the library config). type TranscodeConfig struct { - Enabled bool `toml:"enabled"` // master switch - HWAccel string `toml:"hw_accel"` // "auto" | "none" | "nvenc" | "qsv" | "vaapi" | "videotoolbox" - Preset string `toml:"preset"` // libx264 preset; "veryfast" by default + Enabled bool `toml:"enabled"` // master switch + HWAccel string `toml:"hw_accel"` // "auto" | "none" | "nvenc" | "qsv" | "vaapi" | "videotoolbox" + // Preset is the encoder speed/quality dial. Only used on software encode + // (libx264) — HW backends (NVENC/QSV/VAAPI/VideoToolbox) use vendor + // presets that don't share libx264's vocabulary and would be rejected + // by ffmpeg if passed here. + // + // Empty (default) → engine picks "superfast" — latency-biased, ~3 s + // first-play on 1080p source on a modern x86 CPU. Marginal quality loss + // at 5-25 Mbps target bitrates. + // + // For better quality at slower first-play (1-2 s slower per seg): + // "veryfast" — previous default; balanced + // "faster" — slight quality bump + // "fast" — meaningful quality bump + // "medium" — libx264 stock default; CPU-bound on 4K + // "slow" / "slower" / "veryslow" — only for batch encodes, not real-time HLS + // + // Or faster: + // "ultrafast" — lowest quality, fastest encode + Preset string `toml:"preset"` VideoBitrate string `toml:"video_bitrate"` // e.g. "5M" AudioBitrate string `toml:"audio_bitrate"` // e.g. "192k" MaxHeight int `toml:"max_height"` // optional downscale cap (e.g. 720) @@ -176,7 +194,10 @@ func Default() Config { Transcode: TranscodeConfig{ Enabled: true, HWAccel: "auto", - Preset: "veryfast", + // Empty preset → engine.ResolveEncoderProfile picks the + // latency-biased default ("superfast" on libx264). Override + // in config.toml when quality > first-start latency matters. + Preset: "", AudioBitrate: "192k", MaxConcurrent: 2, }, @@ -280,7 +301,12 @@ func applyDefaults(cfg *Config, meta toml.MetaData) { cfg.Download.Transcode.HWAccel = "auto" } if !meta.IsDefined("downloads", "transcode", "preset") { - cfg.Download.Transcode.Preset = "veryfast" + // Empty = let engine.ResolveEncoderProfile pick the latency-biased + // default ("superfast" on libx264). Users wanting better quality at + // slower first-play can override to "veryfast" / "fast" / "medium" in + // config.toml. Ignored when hw_accel picks NVENC/QSV/VAAPI/VideoToolbox + // (those have built-in vendor presets). + cfg.Download.Transcode.Preset = "" } if !meta.IsDefined("downloads", "transcode", "audio_bitrate") { cfg.Download.Transcode.AudioBitrate = "192k" diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 8097395..c43599f 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -215,8 +215,11 @@ name = "Test" if cfg.Download.Transcode.HWAccel != "auto" { t.Errorf("Transcode.HWAccel = %q, want auto", cfg.Download.Transcode.HWAccel) } - if cfg.Download.Transcode.Preset != "veryfast" { - t.Errorf("Transcode.Preset = %q, want veryfast", cfg.Download.Transcode.Preset) + if cfg.Download.Transcode.Preset != "" { + // Default is now empty — engine.ResolveEncoderProfile picks + // "superfast" on libx264 for first-start latency. Users + // wanting better quality override in config.toml. + t.Errorf("Transcode.Preset = %q, want empty", cfg.Download.Transcode.Preset) } if cfg.Download.Transcode.MaxConcurrent != 2 { t.Errorf("Transcode.MaxConcurrent = %d, want 2", cfg.Download.Transcode.MaxConcurrent) diff --git a/internal/engine/hls.go b/internal/engine/hls.go index 61ce4d3..cbb4501 100644 --- a/internal/engine/hls.go +++ b/internal/engine/hls.go @@ -987,27 +987,31 @@ type EncoderProfile struct { // ResolveEncoderProfile mirrors the codec + preset selection inside // buildHLSFFmpegArgsAt so callers (registry, log lines, diagnostic // endpoints) can know what ffmpeg will be told to do without parsing argv. +// +// The configured preset is libx264-specific by vocabulary (ultrafast… +// veryslow). Passing it through to NVENC / QSV would have ffmpeg reject +// the argv (NVENC uses p1-p7, QSV uses its own subset). So vendor encoders +// always use their hardcoded vendor preset and ignore configuredPreset. +// VideoToolbox has no preset knob at all. func ResolveEncoderProfile(hw HWAccel, configuredPreset string) EncoderProfile { codec := hw.FFmpegVideoCodec("h264") - preset := configuredPreset switch codec { case "libx264": + preset := configuredPreset if preset == "" { preset = "superfast" } + return EncoderProfile{Codec: codec, Preset: preset} case "h264_nvenc": - if preset == "" { - preset = "p3" - } + return EncoderProfile{Codec: codec, Preset: "p3"} case "h264_qsv": - if preset == "" { - preset = "veryfast" - } + return EncoderProfile{Codec: codec, Preset: "veryfast"} case "h264_videotoolbox": // No preset knob for VideoToolbox; the speed/quality dial is `-q:v`. - preset = "" + return EncoderProfile{Codec: codec, Preset: ""} } - return EncoderProfile{Codec: codec, Preset: preset} + // VAAPI + future codecs: no preset, vendor-specific knobs handled in argv. + return EncoderProfile{Codec: codec, Preset: ""} } // buildHLSFFmpegArgsAt returns the argv for an HLS encode that starts at the diff --git a/internal/engine/hwaccel_test.go b/internal/engine/hwaccel_test.go index bed175c..26c2b6f 100644 --- a/internal/engine/hwaccel_test.go +++ b/internal/engine/hwaccel_test.go @@ -63,15 +63,23 @@ func TestResolveEncoderProfileDefaults(t *testing.T) { } func TestResolveEncoderProfileHonoursConfiguredPreset(t *testing.T) { - // libx264 / NVENC / QSV all defer to the configured preset when set. + // Only libx264 honours the configured preset — the libx264 vocabulary + // (ultrafast…veryslow) doesn't apply to vendor encoders. NVENC has its + // own p1-p7 scale; QSV uses a different subset; VideoToolbox has no + // preset knob. Passing a libx264 preset to them would have ffmpeg reject + // the argv, so ResolveEncoderProfile always falls back to the hardcoded + // vendor preset for non-libx264 codecs. cases := []struct { hw HWAccel configured string wantPreset string }{ - {HWAccelNone, "ultrafast", "ultrafast"}, - {HWAccelNVENC, "p1", "p1"}, - {HWAccelQSV, "veryslow", "veryslow"}, + {HWAccelNone, "ultrafast", "ultrafast"}, // libx264 honours + {HWAccelNone, "medium", "medium"}, // libx264 honours + {HWAccelNVENC, "p1", "p3"}, // NVENC ignores, sticks to p3 + {HWAccelNVENC, "veryfast", "p3"}, // NVENC ignores libx264 vocab + {HWAccelQSV, "veryslow", "veryfast"}, // QSV ignores, sticks to veryfast + {HWAccelVideoToolbox, "veryfast", ""}, // VideoToolbox has no preset } for _, tc := range cases { got := ResolveEncoderProfile(tc.hw, tc.configured)