diff --git a/.github/FFMPEG_GUIDE.md b/.github/FFMPEG_GUIDE.md new file mode 100644 index 0000000..6635b7f --- /dev/null +++ b/.github/FFMPEG_GUIDE.md @@ -0,0 +1,47 @@ +# FFmpeg CI Guide + +This file describes how to provide a vetted FFmpeg build to the CI workflow and how the workflow expects archive layouts. + +## Secrets (recommended) +- `FFMPEG_URL` — primary URL the workflow will download. Use a stable URL to a signed/hosted FFmpeg build. +- `FFMPEG_URL_LINUX` — optional override for Linux runners. +- `FFMPEG_URL_WINDOWS` — optional override for Windows runners. +- `FFMPEG_URL_MACOS` — optional override for macOS runners. + +If per-OS secrets are present, they take precedence over `FFMPEG_URL`. + +## Recommended FFmpeg sources +- Use official static builds from a trusted provider (example): + - Windows (ffmpeg.exe): https://www.gyan.dev/ffmpeg/builds/ + - Linux (static): https://johnvansickle.com/ffmpeg/ + - macOS (static): https://evermeet.cx/ffmpeg/ + +Prefer hosting a copy in your own artifact store (S3, GitHub Releases) so you control the binary used in CI. + +## Expected archive layouts +The workflow will attempt to extract common archive formats. Recommended layouts: + +- Zip containing `ffmpeg.exe` at the archive root + - Example: `ffmpeg-2025-01-01.zip` -> `ffmpeg.exe` (root) + +- Tar.gz or tar.xz containing an `ffmpeg` binary at the archive root or inside a single top-level folder + - Example: `ffmpeg-2025/ffmpeg` or `ffmpeg` + +- Raw binary: a direct link to the `ffmpeg` executable is also supported (the workflow will make it executable). + +If your archive nests the binary deep inside several folders, consider publishing a trimmed archive that places `ffmpeg` at the root for easier CI extraction. + +## Verifying locally +To test the workflow steps locally, download your chosen archive and ensure running the binary prints version information: + +```bash +# on Linux/macOS +./ffmpeg -version + +# on Windows (PowerShell) +.\ffmpeg.exe -version +``` + +## Notes for maintainers +- If you need the workflow to handle a custom archive layout, I can update the extraction step (`.github/workflows/ffmpeg-preflight.yml`) to locate the binary path inside the archive and move it to `src-tauri/resources/ffmpeg(.exe)`. +- After adding secrets, open a PR to trigger the workflow and verify the `FFmpeg preflight OK` message in the CI logs. diff --git a/.github/workflows/ffmpeg-preflight.yml b/.github/workflows/ffmpeg-preflight.yml new file mode 100644 index 0000000..a0d2201 --- /dev/null +++ b/.github/workflows/ffmpeg-preflight.yml @@ -0,0 +1,129 @@ +name: FFmpeg Preflight and Build + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + preflight: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + env: + # Provide a fallback URL via repository secret `FFMPEG_URL_{OS}` or `FFMPEG_URL`. + FFMPEG_URL: ${{ secrets.FFMPEG_URL }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Set up Rust + uses: dtolnay/gh-actions-rs@stable + + - name: Determine OS-specific ffmpeg URL + id: ffmpeg-url + shell: bash + run: | + echo "RUNNER_OS=${RUNNER_OS}" + if [[ "${RUNNER_OS}" == "Windows" ]]; then + echo "url=${{ secrets.FFMPEG_URL_WINDOWS || secrets.FFMPEG_URL }}" >> $GITHUB_OUTPUT + elif [[ "${RUNNER_OS}" == "macOS" ]]; then + echo "url=${{ secrets.FFMPEG_URL_MACOS || secrets.FFMPEG_URL }}" >> $GITHUB_OUTPUT + else + echo "url=${{ secrets.FFMPEG_URL_LINUX || secrets.FFMPEG_URL }}" >> $GITHUB_OUTPUT + fi + + - name: Create resources dir + run: mkdir -p src-tauri/resources + + - name: Download and install FFmpeg into resources + if: steps.ffmpeg-url.outputs.url != '' + shell: bash + run: | + set -euo pipefail + URL="${{ steps.ffmpeg-url.outputs.url }}" + echo "Downloading ffmpeg from: $URL" + FNAME="${RUNNER_TEMP}/ffmpeg_bundle" + if [[ "${RUNNER_OS}" == "Windows" ]]; then + powershell -Command "(New-Object Net.WebClient).DownloadFile('$URL', '$FNAME.zip')" + powershell -Command "Expand-Archive -Path '$FNAME.zip' -DestinationPath '${{ github.workspace }}\\src-tauri\\resources'" + else + curl -sL "$URL" -o "$FNAME" + # Attempt to extract common archive formats + if file "$FNAME" | grep -q 'Zip archive'; then + unzip -q "$FNAME" -d src-tauri/resources + elif file "$FNAME" | grep -q 'gzip compressed data'; then + tar -xzf "$FNAME" -C src-tauri/resources + elif file "$FNAME" | grep -q 'XZ compressed'; then + tar -xJf "$FNAME" -C src-tauri/resources + else + # Assume raw binary + mv "$FNAME" src-tauri/resources/ffmpeg + chmod +x src-tauri/resources/ffmpeg + fi + fi + + - name: List resources + run: ls -la src-tauri/resources || true + + - name: Locate ffmpeg binary (Linux/macOS) + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + # Try to find an ffmpeg executable anywhere under resources + BINPATH=$(find src-tauri/resources -type f -iname ffmpeg -print -quit || true) + if [ -z "$BINPATH" ]; then + BINPATH=$(find src-tauri/resources -type f -iname 'ffmpeg*' -print -quit || true) + fi + if [ -n "$BINPATH" ]; then + echo "Found ffmpeg at $BINPATH" + cp "$BINPATH" src-tauri/resources/ffmpeg + chmod +x src-tauri/resources/ffmpeg + else + echo "ffmpeg binary not found in resources" + ls -R src-tauri/resources || true + exit 1 + fi + + - name: Locate ffmpeg binary (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $found = Get-ChildItem -Path src-tauri/resources -Recurse -Filter ffmpeg.exe -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $found) { + $found = Get-ChildItem -Path src-tauri/resources -Recurse -Filter '*ffmpeg*' -ErrorAction SilentlyContinue | Select-Object -First 1 + } + if ($found) { + Write-Host "Found ffmpeg at $($found.FullName)" + Copy-Item $found.FullName -Destination 'src-tauri\resources\ffmpeg.exe' -Force + } else { + Write-Host "ffmpeg not found in src-tauri/resources" + Get-ChildItem src-tauri\resources -Recurse | Format-List + exit 1 + } + + - name: Install npm deps + run: npm ci + + - name: Copy project FFmpeg helpers + run: node tools/copy-ffmpeg.js || true + + - name: Build Rust and run ffmpeg preflight check + working-directory: src-tauri + run: | + set -e + cargo build --release + cargo run --release --bin check_ffmpeg + + - name: Optional frontend build + run: npm run build --if-present || true diff --git a/src-tauri/src/bin/check_ffmpeg.rs b/src-tauri/src/bin/check_ffmpeg.rs new file mode 100644 index 0000000..e19ab71 --- /dev/null +++ b/src-tauri/src/bin/check_ffmpeg.rs @@ -0,0 +1,13 @@ +fn main() { + // Call the library's FFmpeg preflight check used by the application. + match radio_tauri_lib::player::preflight_ffmpeg_only() { + Ok(()) => { + println!("FFmpeg preflight OK"); + std::process::exit(0); + } + Err(e) => { + eprintln!("FFmpeg preflight failed: {}", e); + std::process::exit(2); + } + } +} diff --git a/tools/copy-ffmpeg.js b/tools/copy-ffmpeg.js index 576bca9..47284c6 100644 --- a/tools/copy-ffmpeg.js +++ b/tools/copy-ffmpeg.js @@ -41,6 +41,31 @@ function resolveSource() { function main() { const name = platformBinName(); + // If CI or prior steps already placed ffmpeg into resources, prefer that and skip copying. + const existingInResources = path.join(resourcesDir, name); + if (exists(existingInResources)) { + console.log(`FFmpeg already present in resources: ${existingInResources} — skipping copy.`); + process.exit(0); + } + // Also search recursively in resources for any ffmpeg-like file (robustness for nested archives) + if (exists(resourcesDir)) { + const files = fs.readdirSync(resourcesDir, { withFileTypes: true }); + const found = (function findRec(dir) { + for (const f of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, f.name); + if (f.isFile() && f.name.toLowerCase().startsWith('ffmpeg')) return p; + if (f.isDirectory()) { + const r = findRec(p); + if (r) return r; + } + } + return null; + })(resourcesDir); + if (found) { + console.log(`Found ffmpeg in resources at ${found} — skipping copy.`); + process.exit(0); + } + } const src = resolveSource(); if (!src) { console.log('FFmpeg not provided; skipping copy (set RADIOPLAYER_FFMPEG or place it under tools/ffmpeg/).');