Skip to content

fix: make libvorbis optional so FFmpeg setup stops failing on Macs - #829

Merged
thcp merged 5 commits into
big-releasefrom
fix/ffmpeg-optional-vorbis-824
Oct 21, 2026
Merged

thcp merged 5 commits into
big-releasefrom
fix/ffmpeg-optional-vorbis-824

Conversation

@thcp

@thcp thcp commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #824

Product change (flagged)

  • libvorbis is no longer required. Without it StemDeck starts and works; only OGG export is unavailable.
  • The export menu shows OGG disabled with the reason "OGG export is unavailable: it needs an FFmpeg with Vorbis support."
  • A forced OGG request (mixdown.ogg, stems/all.zip?format=ogg) answers 422 with an actionable message, no render started.

What changed

  • Shell: REQUIRED_FFMPEG_ENCODERS = pcm_s16le, flac, libmp3lame, aac. libvorbis moves to OPTIONAL_FFMPEG_ENCODERS.
  • Backend: app/pipeline/audio_encoders.py probes ffmpeg -encoders once per binary (cached on ffmpeg_identity()).
  • /api/health gains export_formats: {"wav","mp3","flac","ogg": bool} or null when unknown. No paths.
  • Download errors name the URL and say whether the server was reached (HTTP 429 and other HTTP errors) or the binary was rejected. curl runs --silent, so no progress meter in the message.
  • The macOS setup error ends with the next step: brew install ffmpeg, ffmpeg-full for OGG, STEMDECK_FFMPEG, Rosetta where relevant.
  • Guide: help.exportFormat mentions the greyed-out OGG, all ten tables plus pt-PT.

New macOS order (Apple Silicon and Intel)

  1. STEMDECK_FFMPEG (error if it fails). STEMDECK_FFMPEG_URL skips step 3 and downloads only from that URL.
  2. Data-folder pair that passes the check and is native.
  3. Installed pair that passes, one with libvorbis first:
    • Apple Silicon: /opt/homebrew/bin, /opt/homebrew/opt/ffmpeg-full/bin, /usr/local/bin, /opt/local/bin, PATH. Intel-only binaries skipped.
    • Intel: /usr/local/bin, /usr/local/opt/ffmpeg-full/bin, /opt/local/bin, PATH.
  4. Data-folder pair that runs only via Rosetta (kept, not downloaded over).
  5. Download: shaka per-architecture primary (now passes), then evermeet only on Intel or with Rosetta. Each source staged separately, installed only after the checks.

What did not change

  • Windows and Linux: a system FFmpeg on PATH still needs libvorbis to win over the download. Their downloads include libvorbis, so they pass as before.
  • On Windows and Linux a data-folder pair without libvorbis now passes the check instead of being re-downloaded.
  • Linux curl download errors (runtime pack and FFmpeg) get the new wording. Retries are unchanged.
  • No retries added. Pinned URLs and hashes unchanged. uv.lock untouched, runtime id unchanged. No new dependencies.

Risks

  • A user with Homebrew's plain ffmpeg gets no OGG until they install ffmpeg-full and delete the data-folder pair. Setup does not re-run once a pair passes.
  • The installed-pair search runs fixed paths it did not run before (/usr/local/bin, MacPorts).

Checks (macOS arm64, ffmpeg 9.0.2 without libvorbis)

  • cargo fmt --check, cargo test (179 passed), clippy with the macos-check flags: pass. clippy --tests -D warnings: only the 4 existing errors.
  • pytest: 3333 passed, 15 skipped (the 5 OGG renders skip here). ruff, ruff format, bandit (no medium or high): clean. i18n coverage: clean.
  • Playwright: export-ogg (new), export-menu, help-mode pass. Local full suite: 690 passed, 11 failed. Every failure passes on rerun or also fails on plain big-release (trash-restore x3, footer-time, karaoke, grid-value-box). PR CI e2e shards: green.
  • PR CI: Linux, Windows and macOS Rust Check all green.
  • Real check against an isolated backend: health reports ogg: false, WAV/MP3/FLAC mixdowns and the MP3 zip export, OGG returns 422.

Not tested

  • A real first launch on an Apple Silicon Mac without ffmpeg, an Intel Mac with ffmpeg in /usr/local/bin, and the Rosetta cases. The decision logic is unit-tested for each.

thcp added 5 commits October 21, 2026 23:05
… Intel Macs

- REQUIRED_FFMPEG_ENCODERS is now pcm_s16le, flac, libmp3lame, aac.
  libvorbis moves to OPTIONAL_FFMPEG_ENCODERS: only OGG export uses it,
  and neither the shaka-project primary nor Homebrew's plain ffmpeg has
  it, so every fresh Mac failed first launch.
- macOS installed-pair search per architecture: Apple Silicon
  /opt/homebrew/bin, /opt/homebrew/opt/ffmpeg-full/bin, /usr/local/bin,
  /opt/local/bin; Intel /usr/local/bin, /usr/local/opt/ffmpeg-full/bin,
  /opt/local/bin; then PATH. A pair with libvorbis wins over one without.
- Download sources come from one pure function; the evermeet fallback
  only where its Intel build can run. Each source is staged on its own.
- Errors say whether the server was reached (HTTP 429 and other HTTP
  errors named, with the URL) or the binary was rejected, and end with
  the next step (brew install ffmpeg, ffmpeg-full for OGG, STEMDECK_FFMPEG).
  curl runs with --silent so no progress meter lands in the message.
- Windows and Linux: a system FFmpeg on PATH still has to include
  libvorbis to win over the download, as before.
- The macOS decision functions build under test on every host.
…t libvorbis

- app/pipeline/audio_encoders.py: one probe of `ffmpeg -encoders`, cached
  against ffmpeg_identity(), maps wav/mp3/flac/ogg to their encoders.
  Unknown (no ffmpeg, failed listing) is null, not "unavailable".
- /api/health gains export_formats: {wav, mp3, flac, ogg: bool} or null.
  Booleans only, no path.
- mixdown.<ext> and stems/all.zip answer 422 with an actionable message
  when the ffmpeg lacks the format's encoder, after the job checks (an
  unknown job still 404s) and before any render or archive starts.
- Tests: the five OGG renders skip with a reason when libvorbis is
  absent (skip_without_vorbis); new tests cover the parser, the cache,
  the health field and the refused exports on any machine.
- The export menu reads export_formats from /api/health. With ogg false
  the OGG button stays visible but aria-disabled, with a tooltip and a
  line in the menu giving the reason. A click does nothing, and a
  selected OGG falls back to WAV. Unknown leaves OGG offered.
- New string export.oggUnavailable and an extended help.exportFormat
  Guide sentence in all ten tables (and the pt-PT variant).
- tests/e2e/export-ogg.spec.mjs: disabled with its reason without
  libvorbis, enabled with it, offered when unknown. stubExportFormats in
  helpers.mjs fixes the capability; spec-map.json maps the new module.

Closes #824
The candidate-path test now runs on Linux and Windows too, and the test module only imported env on macOS, so those test targets did not compile.
On Windows ffprobe_beside looks for ffprobe.exe, so the test wrote a file the code never read.
@thcp
thcp marked this pull request as ready for review October 21, 2026 23:17
@thcp
thcp merged commit 047605d into big-release Oct 21, 2026
thcp added a commit that referenced this pull request Oct 22, 2026
…g-release (#836)

Closes #835

## What changed

- **PR to `main`, push to `main`, release:** full suite, as before.
- **PR to any other base:** smoke + changed specs + mapped specs. A hub
module (former `full` source entries: `main.js`, `state.js`, `mixer.js`,
`catalog.js`, `index.html`, `app/api/jobs.py`, `stems.py`,
`app/core/**`, ...) or an unmapped file under `static/` or `app/` now
adds a fixed **core set** (17 specs) instead of everything. `uv.lock`
and `pyproject.toml` add core too.
- **Full suite on a non-main PR** only for a change to how the suite
runs: `playwright.config.mjs`, `tests/e2e/helpers*`, `serve.sh`,
`seed.py`, `package*.json`, the three e2e workflows, the selector,
`spec-map.json`, or unmapped code under `tests/e2e/`.
- **New `e2e-full.yml`:** full suite on every push to `big-release`,
nightly (04:17 UTC) on `big-release`, and `workflow_dispatch` with a
`ref` input.
- **New reusable `e2e.yml`** (`workflow_call`, inputs `mode`,
`base-ref`, `checkout-ref`) holds the steps once. Same pinned SHAs,
`permissions: {}`, no secrets.
- **Concurrency:** a new push to a PR cancels that PR's older e2e run.
Pushes, releases, nightly and manual runs are never cancelled.
`timeout-minutes: 30` per shard.

## What did not change

- Application code, specs, `uv.lock`, package files. Three shards,
`--pass-with-no-tests`.
- Check names gain a prefix: `frontend-e2e / frontend-e2e (1)` to `(3)`.
A reusable workflow always prefixes the caller job. No branch protection
or ruleset requires these names today (checked).

## Real diffs, before and after

| Diff | Files | Before | After |
|---|---|---|---|
| #806 (queue.js, catalog.js) | 5 | FULL 105 | 30 (core), includes
queue-progress |
| #820 | 7 | FULL 105 | 59 (core) |
| #821 | 2 | 47 | 47 |
| #823 | 8 | 5 | 5 |
| #828 (uv.lock) | 4 | 5 | 22 (core) |
| #819, #830, #831 | 7 to 19 | FULL 105 | FULL 105 (edit
`spec-map.json`) |
| #829 | 19 | FULL 105 | FULL 105 (edits `helpers.mjs`) |
| #822 | 33 | FULL 105 | FULL 105 (edits `seed.py`) |
| `main...big-release` | 331 | FULL | FULL (PR to main) |

`npx playwright test --list <selected>` matches the selector exactly for
#806 (30 files, 200 tests) and #820 (59 files, 465 tests). Full suite:
724 tests in 105 files. Core + smoke alone: 159 tests in 22 files.

## Trade-off

A non-main PR can pass while a spec outside its core and mapped sets
breaks. The push to `big-release` run catches it on the merge commit,
minutes later.

## Limits

- **The nightly schedule only starts once `e2e-full.yml` reaches
`main`.** GitHub runs scheduled workflows from the default branch, and
the job checks out `big-release` explicitly for that reason.
- Neither concurrency nor the timeout helps a hosted runner GitHub never
assigns (queue time does not count toward the timeout). Fix by hand:
re-run the job, or `gh run cancel <id>` then `gh run rerun <id>
--failed`.
- This PR changes `ci.yml`, so its own CI runs the full suite. Expected.
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.

1 participant