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_INPUT_SEEKABLE`: HTTP input seekable option, default `0`.
|
||||||
- `FFMPEG_HTTP_RECONNECT`: enable ffmpeg HTTP reconnect options for HTTP inputs, default `1`.
|
- `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_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`.
|
- `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_ENABLED`: probe session duration with ffprobe, default `1` except in `relay`, where default is `0`.
|
||||||
- `METADATA_PROBE_TIMEOUT_MS`: ffprobe duration timeout, default `4000`.
|
- `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
|
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`.
|
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_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_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_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 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_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);
|
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 recentWrite = Promise.resolve();
|
||||||
let favorites = [];
|
let favorites = [];
|
||||||
let favoritesWrite = Promise.resolve();
|
let favoritesWrite = Promise.resolve();
|
||||||
|
let ffmpegSupportsReconnectMaxRetries = false;
|
||||||
|
|
||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
app.use(express.json({ limit: '256kb' }));
|
app.use(express.json({ limit: '256kb' }));
|
||||||
@@ -429,6 +431,7 @@ setInterval(() => {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadRecentUrls(),
|
loadRecentUrls(),
|
||||||
loadFavorites(),
|
loadFavorites(),
|
||||||
|
detectFfmpegHttpReconnectSupport(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
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() {
|
async function loadRecentUrls() {
|
||||||
try {
|
try {
|
||||||
const raw = await fs.readFile(RECENT_URLS_PATH, 'utf8');
|
const raw = await fs.readFile(RECENT_URLS_PATH, 'utf8');
|
||||||
@@ -2507,10 +2584,12 @@ function buildInputArgs(inputUrl, { seekable = true, startTime = 0 } = {}) {
|
|||||||
'1',
|
'1',
|
||||||
'-reconnect_delay_max',
|
'-reconnect_delay_max',
|
||||||
String(FFMPEG_HTTP_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) {
|
if (FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR) {
|
||||||
args.push('-reconnect_on_http_error', 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