diff --git a/server/server.js b/server/server.js index 743a6dd..bd4e2bb 100644 --- a/server/server.js +++ b/server/server.js @@ -325,6 +325,9 @@ app.get('/api/channel', async (c) => { // videoId -> { info, formats:[{formatId,height,vcodec,acodec,abr,ext,url,headers}], expiresAt } const streamCache = new Map(); const STREAM_CACHE_MAX = 200; +// Max bytes per upstream googlevideo request (its adaptive streams are cut +// after ~10 MB; matches yt-dlp's http_chunk_size). +const PLAY_CHUNK = 10 * 1024 * 1024 - 1024; // googlevideo URLs carry `expire=`; return that as an ms epoch. function parseExpiry(url) { @@ -535,7 +538,12 @@ app.get('/api/play', async (c) => { const fmt = pickFormat(formats, sel); if (!fmt) return { error: 'no matching format' }; const headers = { ...fmt.headers }; - if (range) headers['Range'] = range; + // Always ask upstream for a bounded slice (see PLAY_CHUNK below); the + // splicer extends it to the client's full requested range. + const rq = /bytes=(\d+)-(\d*)/.exec(range || ''); + const from = rq ? Number(rq[1]) : 0; + const to = rq && rq[2] !== '' ? Math.min(Number(rq[2]), from + PLAY_CHUNK - 1) : from + PLAY_CHUNK - 1; + headers['Range'] = range && !rq ? range : `bytes=${from}-${to}`; return { fmt, res: await fetch(fmt.url, { headers, redirect: 'follow' }) }; } @@ -560,15 +568,77 @@ app.get('/api/play', async (c) => { const up = attempt.res; const headers = new Headers(); - for (const k of ['content-type', 'content-length', 'content-range', 'accept-ranges']) { - const v = up.headers.get(k); - if (v) headers.set(k, v); - } - if (!headers.has('accept-ranges')) headers.set('Accept-Ranges', 'bytes'); - if (!headers.has('content-type')) headers.set('Content-Type', sel.wantAudio ? 'audio/mp4' : 'video/mp4'); + headers.set('Content-Type', up.headers.get('content-type') || (sel.wantAudio ? 'audio/mp4' : 'video/mp4')); + headers.set('Accept-Ranges', 'bytes'); headers.set('Cache-Control', 'no-store'); - return new Response(up.body, { status: up.status, headers }); + // googlevideo silently closes adaptive-stream connections after ~10 MB + // (yt-dlp's own `http_chunk_size` exists for exactly this), so a single + // forwarded `bytes=0-` truncated at ~30 s of 720p and the