github derekshreds/Snacks v2.15.0
Snacks v2.15.0

3 hours ago

Snacks v2.15.0

Automated Media Library Transcoder

A feature release built around device-target encoding — encoding for a specific playback device rather than just a bitrate. The headline is a new iPod Classic quality preset and the encoder primitives behind it: a fixed frame size (scale-and-letterbox to an exact resolution), a max frame-rate cap, explicit H.264/H.265 profile & level flags, and a per-track audio sample rate. Those old devices need a strict recipe — 640×480, H.264 Baseline profile at Level 3.0, ≤30 fps, AAC stereo at 48 kHz — that the normal "hit a bitrate" pipeline couldn't express. Each primitive is exposed as a first-class setting, a per-folder/per-node cluster override, and is wired into the preset system, which gained a reset floor so switching between a device profile and a general one no longer leaves device-specific settings stuck on. It also ships one Library Health correctness fix: healthy HEVC files no longer get flagged as "verify failed" because of a benign muxer-timestamp complaint during sampling.


Device-target encoding

The iPod Classic preset

A new built-in iPod Classic preset (in the Quality Presets panel) targets the H.264 recipe the Classic / 5th-Gen / Nano generation actually decodes: 640×480 · H.264 Baseline · Level 3.0 · MP4 · 1500 kbps · AAC stereo 160 kbps @ 48 kHz, with HDR tone-mapped to SDR. It composes the new primitives below and — critically — pins HardwareAcceleration = "none", because the profile/level flags are only emitted on software encoders and a hardware encoder's default High-profile stream won't play on these devices (see the hardware-assignment note).

Fixed frame size (scale + letterbox to an exact resolution)

  • EncoderOptions.FixedFrameSize (new, e.g. "640x480") — when set, overrides the normal downscale policy/target and produces an exact frame: TranscodingService.ComputeFixedFrameFilter builds a scale=…:force_original_aspect_ratio=decreasepad=W:H:(ow-iw)/2:(oh-ih)/2format=yuv420p chain that fits the source inside the target with letterboxing and forces the 8-bit 4:2:0 pixel format Baseline (and most hardware players) require. The min() commas are escaped so ffmpeg reads them as expression args, not filter separators.
  • Robust parsing"WxH" is parsed case-insensitively; odd dimensions are rounded down to the nearest even (yuv420p and the pad target require even sizes) so a hand-entered odd value can't make ffmpeg reject the frame at runtime, and an unparseable or rounds-to-zero value returns null so the caller cleanly falls back to ComputeScaleExpr.
  • Exposed as a Fixed Frame Size text input in Video settings and as a per-folder/per-node cluster override.

Max frame-rate cap

  • EncoderOptions.MaxFrameRate (new, 0 = no cap)TranscodingService.ComputeFpsCapExpr emits an fps=N filter only when the source is known to run faster than the cap. Sources at or below the cap — or whose rate ffprobe can't determine — are left untouched, because fps=N duplicates frames to reach N and would silently upsample a 24 fps file to 30. ParseFrameRate handles ffprobe's num/den form (24000/1001, 30/1) and treats 0/0 as unknown. Needed because H.264 Level 3.0 at 640×480 tops out near 33 fps, so a 50/60 fps source must be capped to stay level-conformant.
  • The fps filter is placed right after crop and before scale in VideoFilterBuilder (crop → fps → tonemap → scale), so dropping frames early reduces the work every downstream filter does.
  • Exposed as a Max Frame Rate (fps) input in Video settings and as a cluster override.

H.264/H.265 profile & level

  • EncoderOptions.VideoProfile / VideoLevel (new) — emit -profile:v / -level flags, but only on software encoders (libx264/libx265): hardware encoders (NVENC/VAAPI/QSV/AMF) accept different profile value sets, so passing these through them would risk an unrunnable command.
  • Codec-aware profile validation (IsVideoProfileValidForEncoder) — H.264 (baseline/main/high/…) and HEVC (main/main10/…) accept disjoint profile sets, and passing an H.264 profile to libx265 makes it hard-fail. An incompatible codec+profile pairing is dropped (with a per-item log line) rather than assembled into a command ffmpeg refuses to run.
  • Exposed as H.264/H.265 Profile and Level selects in Video settings and as cluster overrides.

Per-track audio sample rate

  • AudioOutputProfile.SampleRateHz (new, 0 = source) — emits -ar:a:N per output track when set, so a device that requires a specific rate (the iPod's 48 kHz) gets it while 0 leaves the source rate untouched. Threaded through FfprobeService.BuildAudioCodecArgs and the audio-planner re-encode/fallback tuples.
  • Exposed as a Sample Rate select (Source / 44.1 / 48 / 96 kHz) on each audio-output row in both main settings and the cluster override dialog.

Hardware assignment for device profiles

The iPod preset pins HardwareAcceleration = "none" on purpose: H.264 then resolves to libx264, the only encoder path that emits the -profile:v baseline -level 3.0 flags these devices require. Left on a HW setting (e.g. Apple VideoToolbox), the same request resolves to a hardware encoder whose default High profile the devices can't decode. To make this expressible per-scope, HardwareAcceleration is now an overridable folder/node field in the cluster override dialog (it was previously omitted on the assumption each node just auto-detects its own hardware) — a folder can now force software encoding for a device profile, and a vendor mismatch on a given node still falls back to software automatically.


Presets: a reset floor so settings don't linger

Applying a preset only writes the fields the preset actually carries; an absent field keeps whatever the form currently shows. That's fine between full snapshots but leaks between a partial device profile and a general one — switching from iPod Classic to Balanced would otherwise leave 640×480 / Baseline / the fps cap / forced-software stuck on, and a user preset saved before a field existed hits the same bug.

  • PRESET_BASELINE (new) — a frozen floor of every "sticky" encoder field that isn't set by all built-in presets (HardwareAcceleration, StrictBitrate, FixedFrameSize, VideoProfile, VideoLevel, MaxFrameRate, TonemapHdrToSdr, PreserveOriginalAudio, AudioOutputs). It's layered under the preset's own options on every apply (built-in and user) via { ...PRESET_BASELINE, ...options }, so the preset's values always win and only genuinely-absent keys fall back to their default. A modern full snapshot is unaffected.
  • Preset match-highlighting handles the new fieldspresetMatchesForm now deep-compares AudioOutputs (including SampleRateHz) and normalizes null vs an empty text input, so the iPod card highlights correctly when the form matches.

Library Health: benign HEVC verify noise no longer flags files

Rolling verification input-seeks (-ss before -i) into the middle/end of a file and decodes short samples to the null muxer. Seeking into an open-GOP HEVC stream hands the muxer frames whose DTS aren't strictly increasing, so ffmpeg prints "non monotonically increasing dts to muxer". The frames decoded fine — which is exactly what the check verifies — but that line was counted as a defect, producing false "VERIFY FAILED" reports across healthy HEVC libraries.

  • FileHealthService.IsBenignVerifyNoise (new) — recognizes the muxer-timestamp complaint as sampling-method noise, not a decode defect. It's collected into the raw stderr buffer (so the exit-code fallback can tell "printed nothing" from "printed only noise") but filtered out of the returned issues, and the non-zero-exit fallback is gated on the raw output so a benign-noise-only exit doesn't resurface as a failure. Genuine corruption ("Invalid data found", "error while decoding", "corrupt", …) is not matched and still fails the file.

Other changes

  • Lower downscale targets240p and 480p added to the downscale target options (and the 240 height mapping) so device profiles can target small screens; the 4K bitrate multiplier gains a 1x option (needed so the iPod preset doesn't inflate 4K sources).

Tests

  • Snacks.Tests/Video/RateControlAndScaleTests.csComputeFixedFrameFilter (unset/garbage/valid chain, odd→even rounding, rounds-to-zero → null), ComputeFpsCapExpr (disabled, caps when over, no-op at/below cap, untouched when rate unknown), IsVideoProfileValidForEncoder (H.264/HEVC/other matrix), and ParseFrameRate (fraction/whole/junk).
  • Snacks.Tests/Video/VideoFilterTests.cs — fps is placed after crop and before scale; an fps-only chain emits -vf fps=N.
  • Snacks.Tests/Video/HardwareEncoderTests.cs — the iPod preset's HardwareAcceleration = "none" pin resolves H.264 to libx264, where an unpinned Apple host would drop to h264_videotoolbox (no Baseline).
  • Snacks.Tests/Pipeline/FullCommandScenarioTests.cs — full Scenario_iPod_Classic_1080p_to_640x480_baseline end-to-end command assembly.
  • Snacks.Tests/Pipeline/FileHealthVerifyTests.csIsBenignVerifyNoise matches the muxer-DTS line and keeps real errors.
  • Snacks.Tests/Overrides/EncoderOptionOverrideTests.cs — the new override fields apply correctly.

Files Changed

Device-target encoding (encoder primitives)

  • Snacks/Models/EncoderOptions.csVideoProfile, VideoLevel, FixedFrameSize, MaxFrameRate (+ Clone)
  • Snacks/Models/EncoderOptionsOverride.cs — matching nullable overrides + ApplyTo
  • Snacks/Models/AudioOutputProfile.csSampleRateHz (+ Clone)
  • Snacks/Services/TranscodingService.csComputeFixedFrameFilter, ComputeFpsCapExpr, ParseFrameRate, IsVideoProfileValidForEncoder, profile/level emission (software-only), fps wired into the filter chain, 240p height
  • Snacks/Services/VideoFilterBuilder.csfpsExpr param, crop → fps → tonemap → scale order
  • Snacks/Services/FfprobeService.csSampleRateHz threaded through BuildAudioCodecArgs and the re-encode/fallback tuples (-ar:a:N)

Presets

  • Snacks/wwwroot/js/settings/presets.js — iPod Classic preset, PRESET_BASELINE reset floor, AudioOutputs/sample-rate match-highlighting

Settings UI & cluster overrides

  • Snacks/Views/Shared/_VideoSettings.cshtml — Fixed Frame Size, Max Frame Rate, Profile & Level controls; 480p/240p downscale targets
  • Snacks/Views/Shared/_AudioSettings.cshtml — Sample Rate select
  • Snacks/Views/Shared/_GeneralSettings.cshtml1x 4K multiplier option
  • Snacks/Views/Shared/_AppModals.cshtml — override controls for HardwareAcceleration, FixedFrameSize, MaxFrameRate, VideoProfile, VideoLevel, audio Sample Rate; 1x/480p/240p options
  • Snacks/wwwroot/js/cluster/override-dialog.js — new folder/node override fields, MaxFrameRate default, audio sample-rate read/apply
  • Snacks/wwwroot/js/settings/encoder-form.js — get/apply the new fields and audio sample rate

Library Health

  • Snacks/Services/FileHealthService.csIsBenignVerifyNoise, raw-vs-genuine issue filtering, exit-code fallback gated on raw output

Tests

  • Snacks.Tests/Video/RateControlAndScaleTests.cs, Snacks.Tests/Video/VideoFilterTests.cs, Snacks.Tests/Video/HardwareEncoderTests.cs — updated
  • Snacks.Tests/Pipeline/FullCommandScenarioTests.cs, Snacks.Tests/Pipeline/FileHealthVerifyTests.cs (new) — updated/new
  • Snacks.Tests/Overrides/EncoderOptionOverrideTests.cs, Snacks.Tests/Fixtures/ProbeBuilder.cs — updated

Version bumps

  • Snacks/Controllers/HomeController.cs — health endpoint version
  • Snacks/Services/ClusterDiscoveryService.csClusterVersion protocol bump to 2.15.0
  • Snacks/Views/Shared/_Layout.cshtml — footer version
  • README.md — version badge and footer
  • build-and-export.bat — Docker tag version
  • electron-app/package.json / package-lock.json — version

Full documentation: README.md

Don't miss a new Snacks release

NewReleases is sending notifications on new releases.