| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- 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
- };
|