Serialize saves, disable idle timeout, cap savable length at 3h
This commit is contained in:
@@ -47,7 +47,17 @@ const FFMPEG = process.env.FFMPEG_PATH || 'ffmpeg';
|
||||
// whole downlink (~7.6 MB/s measured), and since /api/play fetches its own
|
||||
// googlevideo slices over the same link, one long save starved every
|
||||
// concurrent playback (stalled at ~27 s, 11 KB/s). Leave headroom.
|
||||
const DOWNLOAD_RATE = process.env.DOWNLOAD_RATE || '3M';
|
||||
const DOWNLOAD_RATE = process.env.DOWNLOAD_RATE || '2M';
|
||||
|
||||
// Saves run ONE AT A TIME. Two concurrent rate-capped saves plus playback
|
||||
// still filled the homelab's ~7.5 MB/s downlink and playback starved, so
|
||||
// additional saves wait their turn (the client just sees a longer save).
|
||||
let saveChain = Promise.resolve();
|
||||
function withSaveSlot(fn) {
|
||||
const run = saveChain.then(fn, fn);
|
||||
saveChain = run.catch(() => {});
|
||||
return run;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// BUILD_TAG — must be DETERMINISTIC across restarts of identical code.
|
||||
@@ -682,7 +692,7 @@ app.get('/api/play', async (c) => {
|
||||
// didn't match the extraction client.
|
||||
// Live streams have no end: yt-dlp/ffmpeg would pull the HLS manifest forever
|
||||
// (one such "save" ran for an hour and ate 14 GB). Refuse them up front.
|
||||
const MAX_SAVE_SECONDS = Number(process.env.MAX_SAVE_SECONDS || 4 * 3600);
|
||||
const MAX_SAVE_SECONDS = Number(process.env.MAX_SAVE_SECONDS || 3 * 3600);
|
||||
async function assertNotLive(videoId) {
|
||||
const { info } = await resolveStreams(videoId);
|
||||
if (info.is_live || info.live_status === 'is_live' || info.live_status === 'post_live') {
|
||||
@@ -707,13 +717,16 @@ async function ytdlpDownloadResponse(videoId, fp, formatArgs, signal) {
|
||||
const tmp = `${tmpdir()}/${tmpBase}.mp4`;
|
||||
const ctl = new AbortController();
|
||||
entry = { waiters: 0, tmpBase, ctl };
|
||||
entry.promise = runYtdlp([
|
||||
`https://www.youtube.com/watch?v=${videoId}`,
|
||||
'--no-warnings', '--no-playlist',
|
||||
...formatArgs,
|
||||
'--limit-rate', DOWNLOAD_RATE,
|
||||
'-o', tmp,
|
||||
], { signal: ctl.signal }).then(() => ({ tmp, size: statSync(tmp).size }));
|
||||
entry.promise = withSaveSlot(() => {
|
||||
if (ctl.signal.aborted) throw new Error('save cancelled');
|
||||
return runYtdlp([
|
||||
`https://www.youtube.com/watch?v=${videoId}`,
|
||||
'--no-warnings', '--no-playlist',
|
||||
...formatArgs,
|
||||
'--limit-rate', DOWNLOAD_RATE,
|
||||
'-o', tmp,
|
||||
], { signal: ctl.signal });
|
||||
}).then(() => ({ tmp, size: statSync(tmp).size }));
|
||||
inflightDownloads.set(key, entry);
|
||||
}
|
||||
entry.waiters++;
|
||||
@@ -776,14 +789,14 @@ async function ytdlpEditedDownloadResponse(videoId, fp, keep, signal) {
|
||||
let size, fd;
|
||||
try {
|
||||
// 1) Grab the full source (video+audio merged) so ffmpeg has both streams.
|
||||
await runYtdlp([
|
||||
await withSaveSlot(() => runYtdlp([
|
||||
`https://www.youtube.com/watch?v=${videoId}`,
|
||||
'--no-warnings', '--no-playlist',
|
||||
'-f', 'bv*[height<=720][ext=mp4]+ba[ext=m4a]/bv*[height<=720]+ba/b[ext=mp4]/b',
|
||||
'--merge-output-format', 'mp4',
|
||||
'--limit-rate', DOWNLOAD_RATE,
|
||||
'-o', srcTmp,
|
||||
], { signal });
|
||||
], { signal }));
|
||||
// 2) Trim + concat the keep segments into the final custom video.
|
||||
await runFfmpeg([
|
||||
'-y', '-hide_banner', '-loglevel', 'error',
|
||||
@@ -1223,9 +1236,12 @@ async function main() {
|
||||
port: PORT,
|
||||
fetch: app.fetch,
|
||||
// Default is 10s, which killed /api/download proxy streams whenever the
|
||||
// connection went idle mid-transfer. 240s covers slow saves; Bun caps
|
||||
// this field at 255.
|
||||
idleTimeout: 240,
|
||||
// connection went idle mid-transfer. 240s then killed every save whose
|
||||
// server-side yt-dlp phase (no bytes sent yet) ran longer than 4 min —
|
||||
// long videos at the rate cap — and the client retried in a loop.
|
||||
// 0 disables the idle timeout entirely (verified with a 300s request);
|
||||
// runaway downloads are bounded by MAX_SAVE_SECONDS + kill-on-disconnect.
|
||||
idleTimeout: 0,
|
||||
});
|
||||
|
||||
console.log(`[ytplayer] Listening → http://localhost:${PORT}`);
|
||||
|
||||
Reference in New Issue
Block a user