fix(player):修复直链播放m3u8时跨域错误及分片加载失败的问题
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ export const runtime = 'nodejs';
|
|||||||
* 用于外部播放器访问,会执行去广告逻辑并处理相对链接
|
* 用于外部播放器访问,会执行去广告逻辑并处理相对链接
|
||||||
* GET /api/proxy-m3u8?url=<原始m3u8地址>&source=<播放源>&token=<鉴权token>
|
* GET /api/proxy-m3u8?url=<原始m3u8地址>&source=<播放源>&token=<鉴权token>
|
||||||
*/
|
*/
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const m3u8Url = searchParams.get('url');
|
const m3u8Url = searchParams.get('url');
|
||||||
@@ -38,8 +38,11 @@ export async function GET(request: Request) {
|
|||||||
// 优先级:SITE_BASE 环境变量 > 从请求头构建
|
// 优先级:SITE_BASE 环境变量 > 从请求头构建
|
||||||
let origin = process.env.SITE_BASE;
|
let origin = process.env.SITE_BASE;
|
||||||
if (!origin) {
|
if (!origin) {
|
||||||
const requestUrl = new URL(request.url);
|
// 从请求头中获取 Host 和协议
|
||||||
origin = `${requestUrl.protocol}//${requestUrl.host}`;
|
const host = request.headers.get('host') || request.headers.get('x-forwarded-host');
|
||||||
|
const proto = request.headers.get('x-forwarded-proto') ||
|
||||||
|
(host?.includes('localhost') || host?.includes('127.0.0.1') ? 'http' : 'https');
|
||||||
|
origin = `${proto}://${host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取原始 m3u8 内容
|
// 获取原始 m3u8 内容
|
||||||
@@ -196,11 +199,16 @@ function resolveM3u8Links(m3u8Content: string, baseUrl: string, source: string,
|
|||||||
} else {
|
} else {
|
||||||
keyUri = new URL(keyUri, baseDir).href;
|
keyUri = new URL(keyUri, baseDir).href;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直链播放模式:通过代理访问密钥,避免 CORS 问题
|
||||||
|
if (source === 'directplay') {
|
||||||
|
keyUri = `${proxyOrigin}/api/proxy/vod/segment?url=${encodeURIComponent(keyUri)}&source=directplay`;
|
||||||
|
}
|
||||||
|
|
||||||
// 替换原来的 URI
|
// 替换原来的 URI
|
||||||
line = line.replace(/URI="[^"]+"/, `URI="${keyUri}"`);
|
line = line.replace(/URI="[^"]+"/, `URI="${keyUri}"`);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
resolvedLines.push(line);
|
resolvedLines.push(line);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -240,6 +248,9 @@ function resolveM3u8Links(m3u8Content: string, baseUrl: string, source: string,
|
|||||||
if (isM3u8) {
|
if (isM3u8) {
|
||||||
const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '';
|
const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '';
|
||||||
url = `${proxyOrigin}/api/proxy-m3u8?url=${encodeURIComponent(url)}${source ? `&source=${encodeURIComponent(source)}` : ''}${tokenParam}`;
|
url = `${proxyOrigin}/api/proxy-m3u8?url=${encodeURIComponent(url)}${source ? `&source=${encodeURIComponent(source)}` : ''}${tokenParam}`;
|
||||||
|
} else if (source === 'directplay') {
|
||||||
|
// 直链播放模式:通过代理访问媒体分片(ts/jpeg/png 等),避免 CORS 问题
|
||||||
|
url = `${proxyOrigin}/api/proxy/vod/segment?url=${encodeURIComponent(url)}&source=directplay`;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolvedLines.push(url);
|
resolvedLines.push(url);
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export async function GET(request: Request) {
|
|||||||
return NextResponse.json({ error: 'Missing source' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing source' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 直链播放模式:跳过源站配置检查,直接代理
|
||||||
|
if (source !== 'directplay') {
|
||||||
// 检查该视频源是否启用了代理模式
|
// 检查该视频源是否启用了代理模式
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const videoSource = config.SourceConfig?.find((s: any) => s.key === source);
|
const videoSource = config.SourceConfig?.find((s: any) => s.key === source);
|
||||||
@@ -30,6 +32,7 @@ export async function GET(request: Request) {
|
|||||||
if (!videoSource.proxyMode) {
|
if (!videoSource.proxyMode) {
|
||||||
return NextResponse.json({ error: 'Proxy mode not enabled for this source' }, { status: 403 });
|
return NextResponse.json({ error: 'Proxy mode not enabled for this source' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let response: Response | null = null;
|
let response: Response | null = null;
|
||||||
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
||||||
|
|||||||
+40
-26
@@ -1571,10 +1571,8 @@ function PlayPageClient() {
|
|||||||
console.log('播放源评分排序结果:');
|
console.log('播放源评分排序结果:');
|
||||||
resultsWithScore.forEach((result, index) => {
|
resultsWithScore.forEach((result, index) => {
|
||||||
console.log(
|
console.log(
|
||||||
`${index + 1}. ${
|
`${index + 1}. ${result.source.source_name
|
||||||
result.source.source_name
|
} - 评分: ${result.score.toFixed(2)} (${result.testResult.quality}, ${result.testResult.loadSpeed
|
||||||
} - 评分: ${result.score.toFixed(2)} (${result.testResult.quality}, ${
|
|
||||||
result.testResult.loadSpeed
|
|
||||||
}, ${result.testResult.pingTime}ms)`
|
}, ${result.testResult.pingTime}ms)`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -2171,6 +2169,11 @@ function PlayPageClient() {
|
|||||||
// 如果视频源启用了代理模式,且不是本地下载,则通过代理播放
|
// 如果视频源启用了代理模式,且不是本地下载,则通过代理播放
|
||||||
newUrl = `/api/proxy/vod/m3u8?url=${encodeURIComponent(newUrl)}&source=${encodeURIComponent(currentSource)}`;
|
newUrl = `/api/proxy/vod/m3u8?url=${encodeURIComponent(newUrl)}&source=${encodeURIComponent(currentSource)}`;
|
||||||
console.log('使用代理模式播放:', newUrl);
|
console.log('使用代理模式播放:', newUrl);
|
||||||
|
} else if (currentSource === 'directplay' && newUrl) {
|
||||||
|
// 直链播放模式:通过 proxy-m3u8 代理播放,避免 CORS 问题
|
||||||
|
const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : '';
|
||||||
|
newUrl = `/api/proxy-m3u8?url=${encodeURIComponent(newUrl)}&source=directplay${tokenParam}`;
|
||||||
|
console.log('直链播放使用代理模式:', newUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5041,8 +5044,29 @@ function PlayPageClient() {
|
|||||||
return false;
|
return false;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// 辅助函数:检测代理 URL 是否需要显式声明 m3u8 类型
|
||||||
|
// Artplayer 通过 URL 扩展名自动检测类型,但代理 URL(如 /api/proxy-m3u8?url=...)没有 .m3u8 扩展名
|
||||||
|
const getVideoType = (url: string): string | undefined => {
|
||||||
|
if (!url) return undefined;
|
||||||
|
// 如果 URL 路径中已包含 .m3u8 扩展名,Artplayer 可自动检测,无需显式设置
|
||||||
|
const urlPath = url.split('?')[0];
|
||||||
|
if (urlPath.includes('.m3u8')) return undefined;
|
||||||
|
// 代理 URL 返回的是 m3u8 内容,需要显式声明类型
|
||||||
|
if (url.includes('/api/proxy-m3u8') || url.includes('/api/proxy/vod/m3u8')) {
|
||||||
|
return 'm3u8';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
// 非WebKit浏览器且播放器已存在,使用switch方法切换
|
// 非WebKit浏览器且播放器已存在,使用switch方法切换
|
||||||
if (!isWebkit && artPlayerRef.current) {
|
if (!isWebkit && artPlayerRef.current) {
|
||||||
|
// 显式设置类型,确保代理 URL 能被 HLS.js 正确处理
|
||||||
|
const videoType = getVideoType(videoUrl);
|
||||||
|
if (videoType) {
|
||||||
|
artPlayerRef.current.option.type = videoType;
|
||||||
|
} else {
|
||||||
|
artPlayerRef.current.option.type = '';
|
||||||
|
}
|
||||||
artPlayerRef.current.switch = videoUrl;
|
artPlayerRef.current.switch = videoUrl;
|
||||||
artPlayerRef.current.title = `${videoTitle} - ${playerEpisodeLabel}`;
|
artPlayerRef.current.title = `${videoTitle} - ${playerEpisodeLabel}`;
|
||||||
artPlayerRef.current.poster = videoCover;
|
artPlayerRef.current.poster = videoCover;
|
||||||
@@ -5104,6 +5128,7 @@ function PlayPageClient() {
|
|||||||
artPlayerRef.current = new Artplayer({
|
artPlayerRef.current = new Artplayer({
|
||||||
container: artRef.current!,
|
container: artRef.current!,
|
||||||
url: videoUrl,
|
url: videoUrl,
|
||||||
|
...(getVideoType(videoUrl) ? { type: getVideoType(videoUrl) } : {}),
|
||||||
poster: videoCover,
|
poster: videoCover,
|
||||||
volume: 0.7,
|
volume: 0.7,
|
||||||
isLive: false,
|
isLive: false,
|
||||||
@@ -7224,8 +7249,7 @@ function PlayPageClient() {
|
|||||||
<div className='mb-6 w-80 mx-auto'>
|
<div className='mb-6 w-80 mx-auto'>
|
||||||
<div className='flex justify-center space-x-2 mb-4'>
|
<div className='flex justify-center space-x-2 mb-4'>
|
||||||
<div
|
<div
|
||||||
className={`w-3 h-3 rounded-full transition-all duration-500 ${
|
className={`w-3 h-3 rounded-full transition-all duration-500 ${loadingStage === 'searching' || loadingStage === 'fetching'
|
||||||
loadingStage === 'searching' || loadingStage === 'fetching'
|
|
||||||
? 'bg-green-500 scale-125'
|
? 'bg-green-500 scale-125'
|
||||||
: loadingStage === 'preferring' ||
|
: loadingStage === 'preferring' ||
|
||||||
loadingStage === 'ready'
|
loadingStage === 'ready'
|
||||||
@@ -7234,8 +7258,7 @@ function PlayPageClient() {
|
|||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className={`w-3 h-3 rounded-full transition-all duration-500 ${
|
className={`w-3 h-3 rounded-full transition-all duration-500 ${loadingStage === 'preferring'
|
||||||
loadingStage === 'preferring'
|
|
||||||
? 'bg-green-500 scale-125'
|
? 'bg-green-500 scale-125'
|
||||||
: loadingStage === 'ready'
|
: loadingStage === 'ready'
|
||||||
? 'bg-green-500'
|
? 'bg-green-500'
|
||||||
@@ -7243,8 +7266,7 @@ function PlayPageClient() {
|
|||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className={`w-3 h-3 rounded-full transition-all duration-500 ${
|
className={`w-3 h-3 rounded-full transition-all duration-500 ${loadingStage === 'ready'
|
||||||
loadingStage === 'ready'
|
|
||||||
? 'bg-green-500 scale-125'
|
? 'bg-green-500 scale-125'
|
||||||
: 'bg-gray-300'
|
: 'bg-gray-300'
|
||||||
}`}
|
}`}
|
||||||
@@ -7519,8 +7541,7 @@ function PlayPageClient() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${status === 'completed'
|
||||||
status === 'completed'
|
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
|
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
|
||||||
: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
|
: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
|
||||||
}`}
|
}`}
|
||||||
@@ -7545,8 +7566,7 @@ function PlayPageClient() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className={`w-3.5 h-3.5 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${
|
className={`w-3.5 h-3.5 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${isEpisodeSelectorCollapsed ? 'rotate-180' : 'rotate-0'
|
||||||
isEpisodeSelectorCollapsed ? 'rotate-180' : 'rotate-0'
|
|
||||||
}`}
|
}`}
|
||||||
fill='none'
|
fill='none'
|
||||||
stroke='currentColor'
|
stroke='currentColor'
|
||||||
@@ -7565,8 +7585,7 @@ function PlayPageClient() {
|
|||||||
|
|
||||||
{/* 精致的状态指示点 */}
|
{/* 精致的状态指示点 */}
|
||||||
<div
|
<div
|
||||||
className={`absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full transition-all duration-200 ${
|
className={`absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full transition-all duration-200 ${isEpisodeSelectorCollapsed
|
||||||
isEpisodeSelectorCollapsed
|
|
||||||
? 'bg-orange-400 animate-pulse'
|
? 'bg-orange-400 animate-pulse'
|
||||||
: 'bg-green-400'
|
: 'bg-green-400'
|
||||||
}`}
|
}`}
|
||||||
@@ -7575,16 +7594,14 @@ function PlayPageClient() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`grid gap-4 lg:h-[500px] xl:h-[650px] 2xl:h-[750px] transition-all duration-300 ease-in-out ${
|
className={`grid gap-4 lg:h-[500px] xl:h-[650px] 2xl:h-[750px] transition-all duration-300 ease-in-out ${isEpisodeSelectorCollapsed
|
||||||
isEpisodeSelectorCollapsed
|
|
||||||
? 'grid-cols-1'
|
? 'grid-cols-1'
|
||||||
: 'grid-cols-1 md:grid-cols-4'
|
: 'grid-cols-1 md:grid-cols-4'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* 播放器 */}
|
{/* 播放器 */}
|
||||||
<div
|
<div
|
||||||
className={`transition-all duration-300 ease-in-out rounded-xl border border-white/0 dark:border-white/30 flex flex-col ${
|
className={`transition-all duration-300 ease-in-out rounded-xl border border-white/0 dark:border-white/30 flex flex-col ${isEpisodeSelectorCollapsed ? 'col-span-1' : 'md:col-span-3'
|
||||||
isEpisodeSelectorCollapsed ? 'col-span-1' : 'md:col-span-3'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* 播放器容器 */}
|
{/* 播放器容器 */}
|
||||||
@@ -8034,8 +8051,7 @@ function PlayPageClient() {
|
|||||||
{/* 去广告开关 */}
|
{/* 去广告开关 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setExternalPlayerAdBlock(!externalPlayerAdBlock)}
|
onClick={() => setExternalPlayerAdBlock(!externalPlayerAdBlock)}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all duration-200 shadow-sm hover:shadow-md cursor-pointer border flex-shrink-0 ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all duration-200 shadow-sm hover:shadow-md cursor-pointer border flex-shrink-0 ${externalPlayerAdBlock
|
||||||
externalPlayerAdBlock
|
|
||||||
? 'bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white border-blue-400'
|
? 'bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white border-blue-400'
|
||||||
: 'bg-white hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-200 border-gray-300 dark:border-gray-600'
|
: 'bg-white hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-200 border-gray-300 dark:border-gray-600'
|
||||||
}`}
|
}`}
|
||||||
@@ -8075,8 +8091,7 @@ function PlayPageClient() {
|
|||||||
|
|
||||||
{/* 选集和换源 - 在移动端始终显示,在 lg 及以上可折叠 */}
|
{/* 选集和换源 - 在移动端始终显示,在 lg 及以上可折叠 */}
|
||||||
<div
|
<div
|
||||||
className={`relative z-10 h-[350px] lg:h-full md:overflow-hidden transition-all duration-300 ease-in-out ${
|
className={`relative z-10 h-[350px] lg:h-full md:overflow-hidden transition-all duration-300 ease-in-out ${isEpisodeSelectorCollapsed
|
||||||
isEpisodeSelectorCollapsed
|
|
||||||
? 'md:col-span-1 lg:hidden lg:opacity-0 lg:scale-95'
|
? 'md:col-span-1 lg:hidden lg:opacity-0 lg:scale-95'
|
||||||
: 'md:col-span-1 lg:opacity-100 lg:scale-100'
|
: 'md:col-span-1 lg:opacity-100 lg:scale-100'
|
||||||
}`}
|
}`}
|
||||||
@@ -8273,8 +8288,7 @@ function PlayPageClient() {
|
|||||||
)}
|
)}
|
||||||
{detail?.source_name && (
|
{detail?.source_name && (
|
||||||
<span
|
<span
|
||||||
className={`relative group cursor-pointer border px-2 py-[1px] rounded ${
|
className={`relative group cursor-pointer border px-2 py-[1px] rounded ${detail.source === 'xiaoya' ? 'border-blue-500' : detail.source === 'openlist' || detail.source === 'emby' || detail.source?.startsWith('emby_') ? 'border-yellow-500' : 'border-gray-500/60'
|
||||||
detail.source === 'xiaoya' ? 'border-blue-500' : detail.source === 'openlist' || detail.source === 'emby' || detail.source?.startsWith('emby_') ? 'border-yellow-500' : 'border-gray-500/60'
|
|
||||||
}`}
|
}`}
|
||||||
onClick={fetchCurrentSourceVideoInfo}
|
onClick={fetchCurrentSourceVideoInfo}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user