diff --git a/server/server.js b/server/server.js index 5fd089f..1de2a08 100644 --- a/server/server.js +++ b/server/server.js @@ -760,6 +760,20 @@ async function assertNotLive(videoId) { } } +// Format selectors for saves. `[ext=mp4]` alone is NOT enough: YouTube ships +// AV1 in mp4 containers too, and the bot-check fallback clients (web_embedded) +// rank the AV1 ladder as "bestvideo" — an AV1 save plays on almost nothing +// (iOS Safari has no AV1 decoder), so cached copies errored and fell back to +// streaming. Demand h264 (`vcodec^=avc1`) + m4a/aac first; anything exotic is +// a last resort for videos with no avc ladder at all. +const SAVE_MUX_FORMAT = + 'bv*[height<=720][vcodec^=avc1]+ba[ext=m4a]/bv*[height<=720][vcodec^=avc1]+ba/' + + 'b[ext=mp4][vcodec^=avc1]/bv*[height<=720]+ba/b[ext=mp4]/b'; +const SAVE_DEFAULT_FORMAT = + 'best[ext=mp4][vcodec^=avc1][acodec!=none]/' + + 'bv*[height<=720][vcodec^=avc1]+ba[ext=m4a]/bv*[height<=720][vcodec^=avc1]+ba/' + + 'best[ext=mp4][acodec!=none]/bv*[height<=720]+ba/b'; + // Same video + same format args requested while a download is already in // flight (the OPFS worker retries, then the main-thread fallback retries // again) share ONE yt-dlp run instead of spawning another each time. @@ -849,7 +863,7 @@ async function ytdlpEditedDownloadResponse(videoId, fp, keep, signal) { await withSaveSlot(() => runYtdlpResilient([ `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', + '-f', SAVE_MUX_FORMAT, '--merge-output-format', 'mp4', '--limit-rate', DOWNLOAD_RATE, '-o', srcTmp, @@ -916,7 +930,7 @@ app.get('/api/download/:videoId', async (c) => { if (c.req.query('mux') === '1') { try { return await ytdlpDownloadResponse(videoId, fp, [ - '-f', 'bv*[height<=720][ext=mp4]+ba[ext=m4a]/bv*[height<=720]+ba/b[ext=mp4]/b', + '-f', SAVE_MUX_FORMAT, '--merge-output-format', 'mp4', ], c.req.raw.signal); } catch (err) { @@ -931,8 +945,7 @@ app.get('/api/download/:videoId', async (c) => { // "Requested format is not available". try { return await ytdlpDownloadResponse(videoId, fp, [ - '-f', 'bestvideo[ext=mp4][acodec!=none]/bestvideo[acodec!=none]/best[ext=mp4][acodec!=none]/' - + 'bv*[height<=720][ext=mp4]+ba[ext=m4a]/bv*[height<=720]+ba/b', + '-f', SAVE_DEFAULT_FORMAT, '--merge-output-format', 'mp4', ], c.req.raw.signal); } catch (err) {