Skip to content

feat: Smarter ffmpeg/ffprobe resolution, until we bundle it#141

Draft
Hazer wants to merge 27 commits into
LizardByte:masterfrom
Hazer:feat/transcode-ffmpeg-resolution
Draft

feat: Smarter ffmpeg/ffprobe resolution, until we bundle it#141
Hazer wants to merge 27 commits into
LizardByte:masterfrom
Hazer:feat/transcode-ffmpeg-resolution

Conversation

@Hazer

@Hazer Hazer commented Jun 22, 2026

Copy link
Copy Markdown

Description

Fixes two interrelated transcoding failures and the deeper root cause behind them, plus adds a guided FFmpeg/ffprobe setup experience. Minor refactors or code changes made while developing this new ffmpeg resolution experience.

The reported bugs: playing an HEVC-in-MKV item (a) failed to start transcoding (No such file or directory / ENOENT at spawn) and (b) left the web player on a black screen with no error.

Root cause (deeper than it first appeared):

  1. ffmpeg not resolvable. The default ffmpeg_path is the bare name ffmpeg. On macOS, GUI-launched .app bundles do not inherit the user's shell PATH, so a bare-name lookup fails even when ffmpeg is installed at /opt/homebrew/bin/ffmpeg. The same applies to ffprobe.
  2. ffprobe missing silently corrupts the catalog. When the scanner runs without ffprobe, extract_metadata returns default_metadata (all-NULL fields), so every cataloged file gets video_codec=NULL, audio_codec=NULL, container=NULL, metadata_json=NULL. The playability decision then falls back to an extension check, returns an untrustworthy "transcode required," and playback opens a doomed player that black-screens.

What this PR does:

  • ffmpeg_resolve module — a single source of truth for locating ffmpeg/ffprobe, used by both capability display and the actual spawn. Resolution is literal (configured-absolute → PATH → Missing); it does not silently substitute a binary. A hardcoded binary-name allow-list (ffmpeg, ffprobe) prevents path-lookup abuse. Command::args bypasses the shell, so source paths with spaces/brackets are passed verbatim (the "spaces in path" framing in the original report was a red herring — pinned by a test).
  • ffprobe as a first-class blocker + decision guard. PlaybackDecision gains analysis_state: analyzed | awaiting_analysis. When a source file was never probed (metadata_json IS NULL), get_playback_decision returns awaiting_analysis instead of an untrustworthy "transcode required" — the client refuses to open a doomed player and surfaces a setup hint. This replaces an earlier, buggy client-side capabilities-cache preflight (single source of truth now lives on the server).
  • Auto re-probe pipeline. reprobe_media_files runs ffprobe over rows with NULL metadata_json and writes only the codec/container/stream columns via a targeted UPDATE (independent of the scan path's hash-skip guard, which would otherwise skip content-unchanged files). Triggers: server startup (if ffprobe is now available and unprobed files exist), ffprobe settings change, and a manual "Re-probe media info" button in Settings. Progress surfaces in Dashboard → Activities. Resumable — a crash mid-re-probe simply resumes on the next start.
  • Guided FFmpeg setup UI. A "Detect ffmpeg" action (admin) searches PATH + well-known directories and validates the configured paths, rendering results as a table (directory | ffmpeg version | ffprobe version | encoder tags | Use button). Selecting a row fills both path fields. Per-path validation lines show resolved/version/encoder detail below each input.
  • Structured transcode errors. Spawn failures return a structured TranscodeErrorBody { code, message, action } (e.g. transcode_executable_missing with action: open_settings) instead of a bare 500. The client surfaces these via the in-player overlay and the global banner (with a conditional "Open settings" button for actionable codes). Two inert seams (per-session error store + shared error-mapper) are laid for a deferred lifecycle/mid-stream-failure phase.
  • Endpoints: POST /api/v1/system/tools/discover (admin), GET /api/v1/sessions/<id>/status, GET/POST /api/v1/system/tools/reprobe (admin).

Tests: 340 passing (was ~290 baseline). New coverage: resolver resolution (hermetic, tempfile shims), transcode arg generation (incl. the spaces-in-path invariant), probe_encoders parsing, structured error mapping, route integration (discover auth-gating, session-status 404). A real-ffmpeg smoke test sits behind an opt-in real-ffmpeg-tests feature flag. (TODO: I'll re-check this, AI claim here that I believe is not entirely correct)

Design docs included (can be deleted, included by mistake originally): docs/superpowers/specs/ (the shipped phase + a fully-drafted deferred lifecycle discovery/plan), docs/refactoring/routes-module-split-plan.md (a planned, separately-delegated split of the 4.8k-line routes/media.rs).

Out of scope (deliberately deferred): transcode mid-stream failure handling (first-byte race, mid-stream watcher — drafted in the lifecycle spec); the routes-module split; a planned DB-abstraction + actor-model refactor.

Screenshot

Settings → FFmpeg with the Detect table (versions, encoder tags, per-path validation) and the Re-probe button:

┌─ FFmpeg ────────────────────── [🔍 Detect ffmpeg] ─┐
│ Detected transcoding tools. Select a row to fill   │
│ the paths below.                                    │
│                                                      │
│  Directory      | ffmpeg    | ffprobe   | Encoders    | Action │
│  /opt/homebrew | 8.1.1 ✓   | 8.1.1 ✓  | [H.264][H.265][AV1][Opus][MP3][AAC] | [Use] │
│                                                      │
│  ffmpeg path  [/opt/homebrew/bin/ffmpeg___________] │
│  ✓ found ffmpeg version 8.1.1 · encoders: H.264,…   │
│  ffprobe path [/opt/homebrew/bin/ffprobe__________] │
│  ✓ found ffprobe version 8.1.1                      │
└──────────────────────────────────────────────────────┘

Issues Fixed or Closed

Roadmap Issues

Type of Change

  • feat: New feature (non-breaking change which adds functionality)
  • fix: Bug fix (non-breaking change which fixes an issue)
  • docs: Documentation only changes
  • style: Changes that do not affect the meaning of the code (white-space, formatting, missing semicolons, etc.)
  • refactor: Code change that neither fixes a bug nor adds a feature
  • perf: Code change that improves performance
  • test: Adding missing tests or correcting existing tests
  • build: Changes that affect the build system or external dependencies
  • ci: Changes to CI configuration files and scripts
  • chore: Other changes that don't modify src or test files
  • revert: Reverts a previous commit
  • BREAKING CHANGE: Introduces a breaking change (can be combined with any type above)

Checklist

  • Code follows the style guidelines of this project
  • Code has been self-reviewed
  • Code has been commented, particularly in hard-to-understand areas
  • Code docstring/documentation-blocks for new or existing methods/components have been added or updated
  • Unit tests have been added or updated for any new or modified functionality

AI Usage

  • None: No AI tools were used in creating this PR
  • Light: AI provided minor assistance (formatting, simple suggestions)
  • Moderate: AI helped with code generation or debugging specific parts
  • Heavy: AI generated most or all of the code changes (heavily guided the coding agent, but haven't touched a single line of code myself, to be really fair on this one)

Hazer added 27 commits June 22, 2026 13:12
- PlaybackAnalysisState enum: refuse to decide media never probed by ffprobe
  (NULL metadata_json), instead of returning an untrustworthy 'transcode required'
- count_media_files_needing_reprobe + reprobe_media_files: targeted UPDATE of
  codec/container/metadata_json columns, independent of the hash-skip scan guard
- Re-probe driver (try_spawn_reprobe) + activity progress (Dashboard bar)
- Triggers: startup (ffprobe now available + unprobed files), manual route
  POST /api/v1/system/tools/reprobe (+ GET status). Settings-change trigger
  dropped (DbConn can't be cloned into a spawn from the route)
- Drop the buggy client-side capabilities-cache preflight; the server-side
  analysis_state guard replaces it with single-source-of-truth
- Remove debug telemetry from create_session / get_session_stream
- Client: analysis_state rendering in player, Re-probe button in settings
db.run borrows &self, so the save doesn't consume db; move it into the
spawn afterward (same pattern as scan_library). No clone needed.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
5 New issues
5 New Code Smells (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants