From ea169593a6c8a7e5205ffa49a62740ab99681796 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Fri, 13 Mar 2026 20:14:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=9D=E8=AF=95=E4=BF=AE=E5=A4=8Demby?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E5=86=85=E5=AD=98=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/emby/image/[token]/[itemId]/route.ts | 30 ++++- .../api/emby/play/[token]/[filename]/route.ts | 103 +++++++++++++++--- 2 files changed, 114 insertions(+), 19 deletions(-) diff --git a/src/app/api/emby/image/[token]/[itemId]/route.ts b/src/app/api/emby/image/[token]/[itemId]/route.ts index 41285b7..98672f8 100644 --- a/src/app/api/emby/image/[token]/[itemId]/route.ts +++ b/src/app/api/emby/image/[token]/[itemId]/route.ts @@ -84,10 +84,19 @@ export async function GET( 'User-Agent': client.getUserAgent(), }; - // 请求图片 - const imageResponse = await fetch(imageUrl, { - headers: requestHeaders, - }); + // 创建 AbortController 用于超时控制 + const abortController = new AbortController(); + const timeoutId = setTimeout(() => abortController.abort(), 20000); // 20秒超时 + + try { + // 请求图片 + const imageResponse = await fetch(imageUrl, { + headers: requestHeaders, + signal: abortController.signal, + }); + + // 清除超时定时器 + clearTimeout(timeoutId); if (!imageResponse.ok) { console.error('[Emby Image] 获取图片失败:', { @@ -123,6 +132,19 @@ export async function GET( status: imageResponse.status, headers, }); + } catch (error) { + // 清除超时定时器 + clearTimeout(timeoutId); + + if (error instanceof Error && error.name === 'AbortError') { + console.error('[Emby Image] 请求超时'); + return NextResponse.json( + { error: '请求超时' }, + { status: 504 } + ); + } + throw error; + } } catch (error) { console.error('[Emby Image] 错误:', error); return NextResponse.json( diff --git a/src/app/api/emby/play/[token]/[filename]/route.ts b/src/app/api/emby/play/[token]/[filename]/route.ts index 0d169df..62e43ae 100644 --- a/src/app/api/emby/play/[token]/[filename]/route.ts +++ b/src/app/api/emby/play/[token]/[filename]/route.ts @@ -92,22 +92,42 @@ export async function GET( requestHeaders['Range'] = rangeHeader; } - // 流式代理视频内容 - let videoResponse = await fetch(embyStreamUrl, { - headers: requestHeaders, - }); + // 创建 AbortController 用于超时控制 + const abortController = new AbortController(); + const timeoutId = setTimeout(() => abortController.abort(), 300000); // 5分钟超时 - // 如果返回 401,尝试重新认证并重试 - if (videoResponse.status === 401) { - console.log('[Emby Play] 收到 401 错误,尝试重新认证'); - const { embyManager } = await import('@/lib/emby-manager'); - embyManager.clearCache(); - client = await getEmbyClient(embyKey); - embyStreamUrl = await client.getStreamUrl(itemId, true, true); - videoResponse = await fetch(embyStreamUrl, { + try { + // 流式代理视频内容 + let videoResponse = await fetch(embyStreamUrl, { headers: requestHeaders, + signal: abortController.signal, }); - } + + // 如果返回 401,尝试重新认证并重试 + if (videoResponse.status === 401) { + console.log('[Emby Play] 收到 401 错误,尝试重新认证'); + const { embyManager } = await import('@/lib/emby-manager'); + embyManager.clearCache(); + client = await getEmbyClient(embyKey); + embyStreamUrl = await client.getStreamUrl(itemId, true, true); + + // 重置超时 + clearTimeout(timeoutId); + const retryAbortController = new AbortController(); + const retryTimeoutId = setTimeout(() => retryAbortController.abort(), 300000); + + try { + videoResponse = await fetch(embyStreamUrl, { + headers: requestHeaders, + signal: retryAbortController.signal, + }); + } finally { + clearTimeout(retryTimeoutId); + } + } + + // 清除超时定时器 + clearTimeout(timeoutId); if (!videoResponse.ok) { console.error('[Emby Play] 获取视频流失败:', { @@ -147,11 +167,64 @@ export async function GET( // 使用 URL 中的文件名 headers.set('Content-Disposition', `inline; filename="${params.filename}"`); - // 流式返回视频内容,不等待下载完成 - return new NextResponse(videoResponse.body, { + // 创建一个可以被中断的流 + const { readable, writable } = new TransformStream(); + const reader = videoResponse.body?.getReader(); + + if (!reader) { + return NextResponse.json( + { error: '无法读取视频流' }, + { status: 500 } + ); + } + + // 异步管道传输,确保在客户端断开时清理资源 + (async () => { + const writer = writable.getWriter(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + await writer.write(value); + } + } catch (error) { + // 客户端断开连接或其他错误 + console.log('[Emby Play] 流传输中断:', error instanceof Error ? error.message : 'Unknown error'); + // 取消上游 fetch,停止继续下载 + try { + await reader.cancel(); + } catch (e) { + // 忽略取消错误 + } + } finally { + // 确保资源被释放 + try { + reader.releaseLock(); + await writer.close(); + } catch (e) { + // 忽略关闭错误 + } + } + })(); + + // 流式返回视频内容 + return new NextResponse(readable, { status: videoResponse.status, headers, }); + } catch (error) { + // 清除超时定时器 + clearTimeout(timeoutId); + + if (error instanceof Error && error.name === 'AbortError') { + console.error('[Emby Play] 请求超时'); + return NextResponse.json( + { error: '请求超时' }, + { status: 504 } + ); + } + throw error; + } } catch (error) { console.error('[Emby Play] 错误:', error); return NextResponse.json(