Omit unsupported ffmpeg reconnect retry option

This commit is contained in:
2026-06-12 20:04:53 -07:00
parent 6135a6f535
commit c13bafe96b
3 changed files with 83 additions and 4 deletions

View File

@@ -28,6 +28,7 @@ const FFMPEG_HTTP_RECONNECT = parseBoolean(process.env.FFMPEG_HTTP_RECONNECT, tr
const FFMPEG_HTTP_RECONNECT_DELAY_MAX = clampInteger(process.env.FFMPEG_HTTP_RECONNECT_DELAY_MAX, 2, 0, 30);
const FFMPEG_HTTP_RECONNECT_MAX_RETRIES = clampInteger(process.env.FFMPEG_HTTP_RECONNECT_MAX_RETRIES, 4, 0, 20);
const FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR = process.env.FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR ?? '5xx';
const FFMPEG_CAPABILITY_TIMEOUT_MS = 3000;
const PLAYBACK_CONNECTION_MODE = parsePlaybackConnectionMode(process.env.PLAYBACK_CONNECTION_MODE ?? process.env.PLAYBACK_MODE);
const METADATA_PROBE_ENABLED = parseBoolean(process.env.METADATA_PROBE_ENABLED, PLAYBACK_CONNECTION_MODE !== 'relay');
const METADATA_PROBE_TIMEOUT_MS = clampInteger(process.env.METADATA_PROBE_TIMEOUT_MS, 4 * 1000, 1000, 30 * 1000);
@@ -77,6 +78,7 @@ let recentUrls = [];
let recentWrite = Promise.resolve();
let favorites = [];
let favoritesWrite = Promise.resolve();
let ffmpegSupportsReconnectMaxRetries = false;
app.disable('x-powered-by');
app.use(express.json({ limit: '256kb' }));
@@ -429,6 +431,7 @@ setInterval(() => {
await Promise.all([
loadRecentUrls(),
loadFavorites(),
detectFfmpegHttpReconnectSupport(),
]);
server.listen(PORT, () => {
@@ -889,6 +892,80 @@ function runFfprobe(args) {
});
}
async function detectFfmpegHttpReconnectSupport() {
if (!FFMPEG_HTTP_RECONNECT || FFMPEG_HTTP_RECONNECT_MAX_RETRIES <= 0) {
return;
}
try {
const output = await runFfmpegCapabilityCheck(['-hide_banner', '-h', 'protocol=http']);
ffmpegSupportsReconnectMaxRetries = /^\s*-reconnect_max_retries\b/m.test(output);
if (!ffmpegSupportsReconnectMaxRetries) {
logWarn('ffmpeg http option reconnect_max_retries is unsupported; omitting FFMPEG_HTTP_RECONNECT_MAX_RETRIES');
}
} catch (error) {
ffmpegSupportsReconnectMaxRetries = false;
logWarn(`ffmpeg http option detection failed; omitting FFMPEG_HTTP_RECONNECT_MAX_RETRIES error=${oneLine(error.message)}`);
}
}
function runFfmpegCapabilityCheck(args) {
return new Promise((resolve, reject) => {
const ffmpeg = spawn(FFMPEG_PATH, args, {
stdio: ['ignore', 'pipe', 'pipe'],
});
let output = '';
let timedOut = false;
let settled = false;
const finish = (callback) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
callback();
};
const timer = setTimeout(() => {
timedOut = true;
stopProcess(ffmpeg);
}, FFMPEG_CAPABILITY_TIMEOUT_MS);
timer.unref();
ffmpeg.stdout.on('data', (chunk) => {
output = appendTail(output, chunk);
});
ffmpeg.stderr.on('data', (chunk) => {
output = appendTail(output, chunk);
});
ffmpeg.on('error', (error) => {
finish(() => reject(error));
});
ffmpeg.on('close', (code, signal) => {
finish(() => {
if (timedOut) {
reject(new Error('ffmpeg capability check timed out.'));
return;
}
if (code === 0) {
resolve(output);
return;
}
const detail = redactSecrets(output).trim();
reject(new Error(detail ? `ffmpeg capability check exited with code ${code}: ${detail}` : `ffmpeg capability check exited with code ${code ?? 'null'} signal ${signal ?? 'none'}.`));
});
});
});
}
async function loadRecentUrls() {
try {
const raw = await fs.readFile(RECENT_URLS_PATH, 'utf8');
@@ -2507,10 +2584,12 @@ function buildInputArgs(inputUrl, { seekable = true, startTime = 0 } = {}) {
'1',
'-reconnect_delay_max',
String(FFMPEG_HTTP_RECONNECT_DELAY_MAX),
'-reconnect_max_retries',
String(FFMPEG_HTTP_RECONNECT_MAX_RETRIES),
);
if (FFMPEG_HTTP_RECONNECT_MAX_RETRIES > 0 && ffmpegSupportsReconnectMaxRetries) {
args.push('-reconnect_max_retries', String(FFMPEG_HTTP_RECONNECT_MAX_RETRIES));
}
if (FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR) {
args.push('-reconnect_on_http_error', FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR);
}