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.ComputeFixedFrameFilterbuilds ascale=…:force_original_aspect_ratio=decrease→pad=W:H:(ow-iw)/2:(oh-ih)/2→format=yuv420pchain 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. Themin()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 returnsnullso the caller cleanly falls back toComputeScaleExpr. - 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.ComputeFpsCapExpremits anfps=Nfilter 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, becausefps=Nduplicates frames to reach N and would silently upsample a 24 fps file to 30.ParseFrameRatehandles ffprobe'snum/denform (24000/1001,30/1) and treats0/0as 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/-levelflags, 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:Nper output track when set, so a device that requires a specific rate (the iPod's 48 kHz) gets it while0leaves the source rate untouched. Threaded throughFfprobeService.BuildAudioCodecArgsand 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 fields —
presetMatchesFormnow deep-comparesAudioOutputs(includingSampleRateHz) and normalizesnullvs 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 targets —
240pand480padded to the downscale target options (and the240height 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.cs—ComputeFixedFrameFilter(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), andParseFrameRate(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'sHardwareAcceleration = "none"pin resolves H.264 tolibx264, where an unpinned Apple host would drop toh264_videotoolbox(no Baseline).Snacks.Tests/Pipeline/FullCommandScenarioTests.cs— fullScenario_iPod_Classic_1080p_to_640x480_baselineend-to-end command assembly.Snacks.Tests/Pipeline/FileHealthVerifyTests.cs—IsBenignVerifyNoisematches 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.cs—VideoProfile,VideoLevel,FixedFrameSize,MaxFrameRate(+Clone)Snacks/Models/EncoderOptionsOverride.cs— matching nullable overrides +ApplyToSnacks/Models/AudioOutputProfile.cs—SampleRateHz(+Clone)Snacks/Services/TranscodingService.cs—ComputeFixedFrameFilter,ComputeFpsCapExpr,ParseFrameRate,IsVideoProfileValidForEncoder, profile/level emission (software-only), fps wired into the filter chain,240pheightSnacks/Services/VideoFilterBuilder.cs—fpsExprparam, crop → fps → tonemap → scale orderSnacks/Services/FfprobeService.cs—SampleRateHzthreaded throughBuildAudioCodecArgsand the re-encode/fallback tuples (-ar:a:N)
Presets
Snacks/wwwroot/js/settings/presets.js— iPod Classic preset,PRESET_BASELINEreset 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/240pdownscale targetsSnacks/Views/Shared/_AudioSettings.cshtml— Sample Rate selectSnacks/Views/Shared/_GeneralSettings.cshtml—1x4K multiplier optionSnacks/Views/Shared/_AppModals.cshtml— override controls for HardwareAcceleration, FixedFrameSize, MaxFrameRate, VideoProfile, VideoLevel, audio Sample Rate;1x/480p/240poptionsSnacks/wwwroot/js/cluster/override-dialog.js— new folder/node override fields,MaxFrameRatedefault, audio sample-rate read/applySnacks/wwwroot/js/settings/encoder-form.js— get/apply the new fields and audio sample rate
Library Health
Snacks/Services/FileHealthService.cs—IsBenignVerifyNoise, 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— updatedSnacks.Tests/Pipeline/FullCommandScenarioTests.cs,Snacks.Tests/Pipeline/FileHealthVerifyTests.cs(new) — updated/newSnacks.Tests/Overrides/EncoderOptionOverrideTests.cs,Snacks.Tests/Fixtures/ProbeBuilder.cs— updated
Version bumps
Snacks/Controllers/HomeController.cs— health endpoint versionSnacks/Services/ClusterDiscoveryService.cs—ClusterVersionprotocol bump to 2.15.0Snacks/Views/Shared/_Layout.cshtml— footer versionREADME.md— version badge and footerbuild-and-export.bat— Docker tag versionelectron-app/package.json/package-lock.json— version
Full documentation: README.md