| … | … | @@ -18,6 +18,7 @@ import { spawn } from 'bare-subprocess' |
| 18 | 18 | import fs from '#node:fs' |
| 19 | 19 | import { join } from '#node:path' |
| 20 | 20 | import os from '#node:os' |
| 21 | import process from '#node:process' |
| 21 | 22 | |
| 22 | 23 | // ⛔ INJECTABLE, like every other module in this fleet that touches the outside: a routing surface |
| 23 | 24 | // is testable with no port bound because the network is a parameter, and spawning is the same kind |
| … | … | @@ -52,14 +53,30 @@ export const systemRun = (cmd, args) => new Promise((resolve) => { |
| 52 | 53 | }) |
| 53 | 54 | }) |
| 54 | 55 | |
| 55 | | export async function available ({ ffmpegPath = 'ffmpeg', ffprobePath = 'ffprobe', run = systemRun } = {}) { |
| 56 | // ⛔ ENV-OVERRIDABLE, AND READ PER CALL. `rpc.js` invokes `available()`, `probe(path)` and |
| 57 | // `encodeLevel(path, level)` with no options, so every caller takes these defaults. On a server that |
| 58 | // is correct — the binary is on PATH. An APK has no PATH: its bundled binary sits at an absolute |
| 59 | // path the app only learns at runtime (the extracted native-lib dir), so without an override there |
| 60 | // is no way to name it and the node can never derive a rung however many binaries it ships. |
| 61 | // |
| 62 | // ⚠ Read INSIDE the function, not captured at import. The Android host sets these while booting the |
| 63 | // worklet, which happens after this module is imported — a value captured at module scope would be |
| 64 | // the default for ever, which is the "env before any import that reads it" trap favela-android's |
| 65 | // node.js already documents. |
| 66 | // |
| 67 | // ⚠ Absent or empty falls back to the previous literals, byte for byte. The three hosts running this |
| 68 | // daemon must see no change. |
| 69 | const ffmpegBin = () => process.env.FAVELA_FFMPEG || 'ffmpeg' |
| 70 | const ffprobeBin = () => process.env.FAVELA_FFPROBE || 'ffprobe' |
| 71 | |
| 72 | export async function available ({ ffmpegPath = ffmpegBin(), ffprobePath = ffprobeBin(), run = systemRun } = {}) { |
| 56 | 73 | const a = await run(ffmpegPath, ['-version']) |
| 57 | 74 | const b = await run(ffprobePath, ['-version']) |
| 58 | 75 | return a.code === 0 && b.code === 0 |
| 59 | 76 | } |
| 60 | 77 | |
| 61 | 78 | /** `{ width, height, duration }` or null. Never throws. */ |
| 62 | | export async function probe (path, { ffprobePath = 'ffprobe', run = systemRun } = {}) { |
| 79 | export async function probe (path, { ffprobePath = ffprobeBin(), run = systemRun } = {}) { |
| 63 | 80 | const r = await run(ffprobePath, [ |
| 64 | 81 | '-v', 'error', '-select_streams', 'v:0', |
| 65 | 82 | '-show_entries', 'stream=width,height', '-show_entries', 'format=duration', |
| … | … | @@ -86,7 +103,7 @@ export async function probe (path, { ffprobePath = 'ffprobe', run = systemRun } |
| 86 | 103 | * ⚠ `-2` on the width keeps the aspect and rounds to an even number, which h264 requires; asking |
| 87 | 104 | * for an odd width fails at encode time with a message about chroma that reads like a corrupt file. |
| 88 | 105 | */ |
| 89 | | export async function encodeLevel (srcPath, level, { ffmpegPath = 'ffmpeg', dir = null, run = systemRun, stat = null } = {}) { |
| 106 | export async function encodeLevel (srcPath, level, { ffmpegPath = ffmpegBin(), dir = null, run = systemRun, stat = null } = {}) { |
| 90 | 107 | const out = join(dir || os.tmpdir(), 'favela-level-' + level.name + '-' + Date.now() + '.mp4') |
| 91 | 108 | const r = await run(ffmpegPath, [ |
| 92 | 109 | '-y', '-i', srcPath, |