Omit unsupported ffmpeg reconnect retry option
This commit is contained in:
@@ -180,7 +180,7 @@ Runtime:
|
||||
- `FFMPEG_INPUT_SEEKABLE`: HTTP input seekable option, default `0`.
|
||||
- `FFMPEG_HTTP_RECONNECT`: enable ffmpeg HTTP reconnect options for HTTP inputs, default `1`.
|
||||
- `FFMPEG_HTTP_RECONNECT_DELAY_MAX`: max ffmpeg reconnect delay, default `2`.
|
||||
- `FFMPEG_HTTP_RECONNECT_MAX_RETRIES`: max ffmpeg reconnect retries, default `4`.
|
||||
- `FFMPEG_HTTP_RECONNECT_MAX_RETRIES`: max ffmpeg reconnect retries, default `4`, applied only when the installed ffmpeg supports `reconnect_max_retries`.
|
||||
- `FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR`: HTTP status list for reconnect, default `5xx`.
|
||||
- `METADATA_PROBE_ENABLED`: probe session duration with ffprobe, default `1` except in `relay`, where default is `0`.
|
||||
- `METADATA_PROBE_TIMEOUT_MS`: ffprobe duration timeout, default `4000`.
|
||||
|
||||
@@ -48,7 +48,7 @@ Recently played URLs and favorites are stored globally by the backend. In Docker
|
||||
docker logs -f frame-stream-player
|
||||
```
|
||||
|
||||
The app sets `FFMPEG_INPUT_SEEKABLE=0` by default so `ffmpeg` reads stream inputs sequentially and avoids extra HTTP range connections. If a specific VOD file requires seeking for metadata, set `FFMPEG_INPUT_SEEKABLE=-1` to restore ffmpeg's automatic behavior. For ffmpeg-owned HTTP inputs, reconnect handling is enabled by default with `FFMPEG_HTTP_RECONNECT=1`.
|
||||
The app sets `FFMPEG_INPUT_SEEKABLE=0` by default so `ffmpeg` reads stream inputs sequentially and avoids extra HTTP range connections. If a specific VOD file requires seeking for metadata, set `FFMPEG_INPUT_SEEKABLE=-1` to restore ffmpeg's automatic behavior. For ffmpeg-owned HTTP inputs, reconnect handling is enabled by default with `FFMPEG_HTTP_RECONNECT=1`. `FFMPEG_HTTP_RECONNECT_MAX_RETRIES` is applied only when the installed `ffmpeg` supports that HTTP protocol option.
|
||||
|
||||
YouTube URLs are resolved server-side with `yt-dlp` before they enter the existing ffmpeg pipeline. Recents and favorites keep the original YouTube URL, while the short-lived playback session uses the resolved media URL and headers returned by `yt-dlp`. Tune the selected format with `YT_DLP_FORMAT` and the resolver timeout with `YT_DLP_TIMEOUT_MS`.
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user