const { Readable } = require('stream'); const { pipeline } = require('stream/promises'); const { Agent } = require('undici'); const CONNECT_TIMEOUT_MS = 30_000; const MAX_ATTEMPTS = 3; const RETRY_DELAYS_MS = [300, 800]; const videoProxyAgent = new Agent({ connect: { timeout: CONNECT_TIMEOUT_MS } }); const PASSTHROUGH_HEADERS = [ 'content-type', 'content-length', 'content-range', 'accept-ranges', 'etag', 'last-modified', 'cache-control' ]; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function networkErrorCode(error) { return error?.cause?.code || error?.code || ''; } function isRetryableNetworkError(error) { return [ 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_SOCKET', 'ECONNRESET', 'ETIMEDOUT', 'ENETUNREACH', 'EHOSTUNREACH' ].includes(networkErrorCode(error)); } async function fetchVideoWithRetry(url, init, deps) { const fetchImpl = deps.fetchImpl || fetch; const maxAttempts = deps.maxAttempts || MAX_ATTEMPTS; let lastError = null; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { try { return await fetchImpl(url, init); } catch (error) { lastError = error; if (!isRetryableNetworkError(error) || attempt >= maxAttempts || init.signal?.aborted) { throw error; } console.warn( `[video-proxy] connect attempt ${attempt}/${maxAttempts} failed: ${error.message}; cause=${networkErrorCode(error)}` ); await (deps.sleep || sleep)(RETRY_DELAYS_MS[Math.min(attempt - 1, RETRY_DELAYS_MS.length - 1)]); } } throw lastError || new Error('视频代理连接失败'); } function registerVideoProxyRoutes(app, deps) { app.get('/api/video-proxy', async (req, res) => { const { url, filename = 'douyin-video.mp4', download } = req.query; if (!url || typeof url !== 'string') { return res.status(400).json({ error: '缺少 url 参数' }); } if (!deps.isSafeRemoteUrl(url)) { return res.status(400).json({ error: '无效的视频地址' }); } const abortController = new AbortController(); const abortUpstream = () => { if (!res.writableEnded) abortController.abort(); }; res.once('close', abortUpstream); try { const upstreamHeaders = { 'Accept': req.headers.accept || '*/*', 'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0', 'Referer': 'https://www.douyin.com/', 'Origin': 'https://www.douyin.com' }; if (req.headers.range) { upstreamHeaders.Range = req.headers.range; } const response = await fetchVideoWithRetry(url, { method: 'GET', headers: upstreamHeaders, redirect: 'follow', dispatcher: deps.dispatcher || videoProxyAgent, signal: abortController.signal }, deps); if (!response.ok && response.status !== 206) { const errorText = await response.text().catch(() => ''); return res.status(response.status).json({ error: '远程视频请求失败', status: response.status, detail: errorText }); } res.status(response.status); for (const headerName of PASSTHROUGH_HEADERS) { const value = response.headers.get(headerName); if (value) res.setHeader(headerName, value); } if (!response.headers.get('content-type')) { res.setHeader('Content-Type', 'video/mp4'); } res.setHeader( 'Content-Disposition', download === '1' ? `attachment; filename*=UTF-8''${encodeURIComponent(filename)}` : `inline; filename*=UTF-8''${encodeURIComponent(filename)}` ); if (!response.body) { return res.end(); } await pipeline(Readable.fromWeb(response.body), res); } catch (error) { if (abortController.signal.aborted && res.destroyed) { return; } const detail = `${error.message}${networkErrorCode(error) ? `; cause=${networkErrorCode(error)}` : ''}`; console.error('[video-proxy] request failed:', detail); if (!res.headersSent) { res.status(500).json({ error: `视频代理失败: ${detail}` }); } else if (!res.destroyed) { res.destroy(); } } finally { res.off('close', abortUpstream); } }); } module.exports = { registerVideoProxyRoutes, fetchVideoWithRetry, isRetryableNetworkError };