feat: support flv and mp4
This commit is contained in:
@@ -32,6 +32,7 @@
|
|||||||
"bs58": "^6.0.0",
|
"bs58": "^6.0.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
"flv.js": "^1.6.2",
|
||||||
"framer-motion": "^12.18.1",
|
"framer-motion": "^12.18.1",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"hls.js": "^1.6.10",
|
"hls.js": "^1.6.10",
|
||||||
|
|||||||
Generated
+21
@@ -47,6 +47,9 @@ importers:
|
|||||||
crypto-js:
|
crypto-js:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
|
flv.js:
|
||||||
|
specifier: ^1.6.2
|
||||||
|
version: 1.6.2
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^12.18.1
|
specifier: ^12.18.1
|
||||||
version: 12.18.1([email protected]([email protected]))([email protected])
|
version: 12.18.1([email protected]([email protected]))([email protected])
|
||||||
@@ -2627,6 +2630,9 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==}
|
resolution: {integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2881,6 +2887,9 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -5124,6 +5133,9 @@ packages:
|
|||||||
webpack-cli:
|
webpack-cli:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==}
|
resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==}
|
||||||
|
|
||||||
@@ -8198,6 +8210,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -8539,6 +8553,11 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
es6-promise: 4.2.8
|
||||||
|
webworkify-webpack: 2.1.5
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-callable: 1.2.7
|
is-callable: 1.2.7
|
||||||
@@ -11127,6 +11146,8 @@ snapshots:
|
|||||||
- esbuild
|
- esbuild
|
||||||
- uglify-js
|
- uglify-js
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
iconv-lite: 0.4.24
|
iconv-lite: 0.4.24
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const url = searchParams.get('url');
|
||||||
|
const source = searchParams.get('moontv-source');
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return NextResponse.json({ error: 'Missing url' }, { status: 400 });
|
||||||
|
}
|
||||||
|
const config = await getConfig();
|
||||||
|
const liveSource = config.LiveConfig?.find((s: any) => s.key === source);
|
||||||
|
if (!liveSource) {
|
||||||
|
return NextResponse.json({ error: 'Source not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
const ua = liveSource.ua || 'AptvPlayer/1.4.10';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decodedUrl = decodeURIComponent(url);
|
||||||
|
|
||||||
|
const response = await fetch(decodedUrl, {
|
||||||
|
cache: 'no-cache',
|
||||||
|
redirect: 'follow',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': ua,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get('Content-Type');
|
||||||
|
if (contentType?.includes('video/mp4')) {
|
||||||
|
return NextResponse.json({ success: true, type: 'mp4' }, { status: 200 });
|
||||||
|
}
|
||||||
|
if (contentType?.includes('video/x-flv')) {
|
||||||
|
return NextResponse.json({ success: true, type: 'flv' }, { status: 200 });
|
||||||
|
}
|
||||||
|
return NextResponse.json({ success: true, type: 'm3u8' }, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,24 +39,41 @@ export async function GET(request: Request) {
|
|||||||
return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 });
|
return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取最终的响应URL(处理重定向后的URL)
|
// rewrite m3u8
|
||||||
const finalUrl = response.url;
|
if (response.headers.get('Content-Type')?.includes('application/vnd.apple.mpegurl')) {
|
||||||
const m3u8Content = await response.text();
|
// 获取最终的响应URL(处理重定向后的URL)
|
||||||
|
const finalUrl = response.url;
|
||||||
|
const m3u8Content = await response.text();
|
||||||
|
|
||||||
// 使用最终的响应URL作为baseUrl,而不是原始的请求URL
|
// 使用最终的响应URL作为baseUrl,而不是原始的请求URL
|
||||||
const baseUrl = getBaseUrl(finalUrl);
|
const baseUrl = getBaseUrl(finalUrl);
|
||||||
|
|
||||||
// 重写 M3U8 内容
|
// 重写 M3U8 内容
|
||||||
const modifiedContent = rewriteM3U8Content(m3u8Content, baseUrl, request, allowCORS);
|
const modifiedContent = rewriteM3U8Content(m3u8Content, baseUrl, request, allowCORS);
|
||||||
|
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.set('Content-Type', 'application/vnd.apple.mpegurl');
|
||||||
|
headers.set('Access-Control-Allow-Origin', '*');
|
||||||
|
headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
|
headers.set('Access-Control-Allow-Headers', 'Content-Type, Range, Origin, Accept');
|
||||||
|
headers.set('Cache-Control', 'no-cache');
|
||||||
|
headers.set('Access-Control-Expose-Headers', 'Content-Length, Content-Range');
|
||||||
|
return new Response(modifiedContent, { headers });
|
||||||
|
}
|
||||||
|
// just proxy
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.set('Content-Type', 'application/vnd.apple.mpegurl');
|
headers.set('Content-Type', response.headers.get('Content-Type') || 'application/vnd.apple.mpegurl');
|
||||||
headers.set('Access-Control-Allow-Origin', '*');
|
headers.set('Access-Control-Allow-Origin', '*');
|
||||||
headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
headers.set('Access-Control-Allow-Headers', 'Content-Type, Range, Origin, Accept');
|
headers.set('Access-Control-Allow-Headers', 'Content-Type, Range, Origin, Accept');
|
||||||
headers.set('Cache-Control', 'no-cache');
|
headers.set('Cache-Control', 'no-cache');
|
||||||
headers.set('Access-Control-Expose-Headers', 'Content-Length, Content-Range');
|
headers.set('Access-Control-Expose-Headers', 'Content-Length, Content-Range');
|
||||||
return new Response(modifiedContent, { headers });
|
|
||||||
|
// 直接返回视频流
|
||||||
|
return new Response(response.body, {
|
||||||
|
status: 200,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 });
|
return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 });
|
||||||
}
|
}
|
||||||
@@ -87,9 +104,7 @@ function rewriteM3U8Content(content: string, baseUrl: string, req: Request, allo
|
|||||||
// 处理 TS 片段 URL 和其他媒体文件
|
// 处理 TS 片段 URL 和其他媒体文件
|
||||||
if (line && !line.startsWith('#')) {
|
if (line && !line.startsWith('#')) {
|
||||||
const resolvedUrl = resolveUrl(baseUrl, line);
|
const resolvedUrl = resolveUrl(baseUrl, line);
|
||||||
// 检查是否为 mp4 格式
|
const proxyUrl = allowCORS ? resolvedUrl : `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}`;
|
||||||
const isMp4 = resolvedUrl.toLowerCase().includes('.mp4') || resolvedUrl.toLowerCase().includes('mp4');
|
|
||||||
const proxyUrl = (isMp4 || allowCORS) ? resolvedUrl : `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}`;
|
|
||||||
rewrittenLines.push(proxyUrl);
|
rewrittenLines.push(proxyUrl);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -133,9 +148,7 @@ function rewriteMapUri(line: string, baseUrl: string, proxyBase: string) {
|
|||||||
if (uriMatch) {
|
if (uriMatch) {
|
||||||
const originalUri = uriMatch[1];
|
const originalUri = uriMatch[1];
|
||||||
const resolvedUrl = resolveUrl(baseUrl, originalUri);
|
const resolvedUrl = resolveUrl(baseUrl, originalUri);
|
||||||
// 检查是否为 mp4 格式,如果是则走 proxyBase
|
const proxyUrl = `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}`;
|
||||||
const isMp4 = resolvedUrl.toLowerCase().includes('.mp4') || resolvedUrl.toLowerCase().includes('mp4');
|
|
||||||
const proxyUrl = isMp4 ? `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}` : `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}`;
|
|
||||||
return line.replace(uriMatch[0], `URI="${proxyUrl}"`);
|
return line.replace(uriMatch[0], `URI="${proxyUrl}"`);
|
||||||
}
|
}
|
||||||
return line;
|
return line;
|
||||||
|
|||||||
+188
-139
@@ -17,6 +17,7 @@ import PageLayout from '@/components/PageLayout';
|
|||||||
declare global {
|
declare global {
|
||||||
interface HTMLVideoElement {
|
interface HTMLVideoElement {
|
||||||
hls?: any;
|
hls?: any;
|
||||||
|
flv?: any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,7 +389,6 @@ function LivePageClient() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('节目单信息:', result.data);
|
|
||||||
// 清洗EPG数据,去除重叠的节目
|
// 清洗EPG数据,去除重叠的节目
|
||||||
const cleanedData = {
|
const cleanedData = {
|
||||||
...result.data,
|
...result.data,
|
||||||
@@ -417,6 +417,9 @@ function LivePageClient() {
|
|||||||
if (artPlayerRef.current.video && artPlayerRef.current.video.hls) {
|
if (artPlayerRef.current.video && artPlayerRef.current.video.hls) {
|
||||||
artPlayerRef.current.video.hls.destroy();
|
artPlayerRef.current.video.hls.destroy();
|
||||||
}
|
}
|
||||||
|
if (artPlayerRef.current.video && artPlayerRef.current.video.flv) {
|
||||||
|
artPlayerRef.current.video.flv.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
// 销毁 ArtPlayer 实例
|
// 销毁 ArtPlayer 实例
|
||||||
artPlayerRef.current.destroy();
|
artPlayerRef.current.destroy();
|
||||||
@@ -535,154 +538,200 @@ function LivePageClient() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放器初始化
|
function m3u8Loader(video: HTMLVideoElement, url: string) {
|
||||||
useEffect(() => {
|
if (!Hls) {
|
||||||
if (
|
console.error('HLS.js 未加载');
|
||||||
!Artplayer ||
|
|
||||||
!Hls ||
|
|
||||||
!videoUrl ||
|
|
||||||
!artRef.current ||
|
|
||||||
!currentChannel
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('视频URL:', videoUrl);
|
if (video.hls) {
|
||||||
|
video.hls.destroy();
|
||||||
// 销毁之前的播放器实例并创建新的
|
|
||||||
if (artPlayerRef.current) {
|
|
||||||
cleanupPlayer();
|
|
||||||
}
|
}
|
||||||
|
const hls = new Hls({
|
||||||
|
debug: false,
|
||||||
|
enableWorker: true,
|
||||||
|
lowLatencyMode: true,
|
||||||
|
maxBufferLength: 30,
|
||||||
|
backBufferLength: 30,
|
||||||
|
maxBufferSize: 60 * 1000 * 1000,
|
||||||
|
loader: CustomHlsJsLoader,
|
||||||
|
});
|
||||||
|
|
||||||
|
hls.loadSource(url);
|
||||||
|
hls.attachMedia(video);
|
||||||
|
video.hls = hls;
|
||||||
|
|
||||||
|
hls.on(Hls.Events.ERROR, function (event: any, data: any) {
|
||||||
|
console.error('HLS Error:', event, data);
|
||||||
|
|
||||||
|
if (data.fatal) {
|
||||||
|
switch (data.type) {
|
||||||
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||||
|
hls.startLoad();
|
||||||
|
break;
|
||||||
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||||
|
hls.recoverMediaError();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
hls.destroy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flvLoader(video: HTMLVideoElement, url: string) {
|
||||||
try {
|
try {
|
||||||
// 创建新的播放器实例
|
const flvjs = await import('flv.js');
|
||||||
Artplayer.USE_RAF = true;
|
const flv = flvjs.default as any;
|
||||||
|
|
||||||
artPlayerRef.current = new Artplayer({
|
if (!flv.isSupported()) {
|
||||||
container: artRef.current,
|
console.error('Flv.js 未支持');
|
||||||
url: videoUrl.toLowerCase().endsWith('.mp4') ? videoUrl : `/api/proxy/m3u8?url=${encodeURIComponent(videoUrl)}&moontv-source=${currentSourceRef.current?.key || ''}`,
|
return;
|
||||||
poster: currentChannel.logo,
|
|
||||||
volume: 0.7,
|
|
||||||
isLive: true, // 设置为直播模式
|
|
||||||
muted: false,
|
|
||||||
autoplay: true,
|
|
||||||
pip: true,
|
|
||||||
autoSize: false,
|
|
||||||
autoMini: false,
|
|
||||||
screenshot: false,
|
|
||||||
setting: false,
|
|
||||||
loop: false,
|
|
||||||
flip: false,
|
|
||||||
playbackRate: false,
|
|
||||||
aspectRatio: false,
|
|
||||||
fullscreen: true,
|
|
||||||
fullscreenWeb: true,
|
|
||||||
subtitleOffset: false,
|
|
||||||
miniProgressBar: false,
|
|
||||||
mutex: true,
|
|
||||||
playsInline: true,
|
|
||||||
autoPlayback: false,
|
|
||||||
airplay: true,
|
|
||||||
theme: '#22c55e',
|
|
||||||
lang: 'zh-cn',
|
|
||||||
hotkey: false,
|
|
||||||
fastForward: false, // 直播不需要快进
|
|
||||||
autoOrientation: true,
|
|
||||||
lock: true,
|
|
||||||
moreVideoAttr: {
|
|
||||||
crossOrigin: 'anonymous',
|
|
||||||
preload: 'metadata',
|
|
||||||
},
|
|
||||||
type: videoUrl.toLowerCase().endsWith('.mp4') ? 'mp4' : 'm3u8',
|
|
||||||
// HLS 支持配置
|
|
||||||
customType: {
|
|
||||||
m3u8: function (video: HTMLVideoElement, url: string) {
|
|
||||||
if (!Hls) {
|
|
||||||
console.error('HLS.js 未加载');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (video.hls) {
|
|
||||||
video.hls.destroy();
|
|
||||||
}
|
|
||||||
const hls = new Hls({
|
|
||||||
debug: false,
|
|
||||||
enableWorker: true,
|
|
||||||
lowLatencyMode: true,
|
|
||||||
maxBufferLength: 30,
|
|
||||||
backBufferLength: 30,
|
|
||||||
maxBufferSize: 60 * 1000 * 1000,
|
|
||||||
loader: CustomHlsJsLoader,
|
|
||||||
});
|
|
||||||
|
|
||||||
hls.loadSource(url);
|
|
||||||
hls.attachMedia(video);
|
|
||||||
video.hls = hls;
|
|
||||||
|
|
||||||
hls.on(Hls.Events.ERROR, function (event: any, data: any) {
|
|
||||||
console.error('HLS Error:', event, data);
|
|
||||||
|
|
||||||
if (data.fatal) {
|
|
||||||
switch (data.type) {
|
|
||||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
||||||
hls.startLoad();
|
|
||||||
break;
|
|
||||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
||||||
hls.recoverMediaError();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
hls.destroy();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
icons: {
|
|
||||||
loading:
|
|
||||||
'<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCIgdmlld0JveD0iMCAwIDUwIDUwIj48cGF0aCBkPSJNMjUuMjUxIDYuNDYxYy0xMC4zMTggMC0xOC42ODMgOC4zNjUtMTguNjgzIDE4LjY4M2g0LjA2OGMwLTguMDcgNi41NDUtMTQuNjE1IDE0LjYxNS0xNC42MTVWNi40NjF6IiBmaWxsPSIjMDA5Njg4Ij48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIGF0dHJpYnV0ZVR5cGU9IlhNTCIgZHVyPSIxcyIgZnJvbT0iMCAyNSAyNSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHRvPSIzNjAgMjUgMjUiIHR5cGU9InJvdGF0ZSIvPjwvcGF0aD48L3N2Zz4=">',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听播放器事件
|
|
||||||
artPlayerRef.current.on('ready', () => {
|
|
||||||
setError(null);
|
|
||||||
setIsVideoLoading(false);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
artPlayerRef.current.on('loadstart', () => {
|
|
||||||
setIsVideoLoading(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
artPlayerRef.current.on('loadeddata', () => {
|
|
||||||
setIsVideoLoading(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
artPlayerRef.current.on('canplay', () => {
|
|
||||||
setIsVideoLoading(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
artPlayerRef.current.on('waiting', () => {
|
|
||||||
setIsVideoLoading(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
artPlayerRef.current.on('error', (err: any) => {
|
|
||||||
console.error('播放器错误:', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (artPlayerRef.current?.video) {
|
|
||||||
const finalUrl = videoUrl.toLowerCase().endsWith('.mp4') ? videoUrl : `/api/proxy/m3u8?url=${encodeURIComponent(videoUrl)}`;
|
|
||||||
ensureVideoSource(
|
|
||||||
artPlayerRef.current.video as HTMLVideoElement,
|
|
||||||
finalUrl
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
if (video.flv) {
|
||||||
console.error('创建播放器失败:', err);
|
video.flv.destroy();
|
||||||
// 不设置错误,只记录日志
|
}
|
||||||
|
|
||||||
|
const flvPlayer = flv.createPlayer({
|
||||||
|
type: 'flv',
|
||||||
|
url: url,
|
||||||
|
});
|
||||||
|
flvPlayer.attachMediaElement(video);
|
||||||
|
flvPlayer.load();
|
||||||
|
video.flv = flvPlayer;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载 Flv.js 失败:', error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放器初始化
|
||||||
|
useEffect(() => {
|
||||||
|
const preload = async () => {
|
||||||
|
if (
|
||||||
|
!Artplayer ||
|
||||||
|
!Hls ||
|
||||||
|
!videoUrl ||
|
||||||
|
!artRef.current ||
|
||||||
|
!currentChannel
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('视频URL:', videoUrl);
|
||||||
|
|
||||||
|
// 销毁之前的播放器实例并创建新的
|
||||||
|
if (artPlayerRef.current) {
|
||||||
|
cleanupPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// precheck type
|
||||||
|
let type = 'm3u8';
|
||||||
|
const precheckUrl = `/api/live/precheck?url=${encodeURIComponent(videoUrl)}&moontv-source=${currentSourceRef.current?.key || ''}`;
|
||||||
|
const precheckResponse = await fetch(precheckUrl);
|
||||||
|
if (!precheckResponse.ok) {
|
||||||
|
console.error('预检查失败:', precheckResponse.statusText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const precheckResult = await precheckResponse.json();
|
||||||
|
if (precheckResult.success) {
|
||||||
|
type = precheckResult.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customType = type === 'flv' ? {
|
||||||
|
flv: flvLoader,
|
||||||
|
} : type === 'mp4' ? {} : {
|
||||||
|
m3u8: m3u8Loader,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
// 创建新的播放器实例
|
||||||
|
Artplayer.USE_RAF = true;
|
||||||
|
|
||||||
|
artPlayerRef.current = new Artplayer({
|
||||||
|
container: artRef.current,
|
||||||
|
url: `/api/proxy/m3u8?url=${encodeURIComponent(videoUrl)}&moontv-source=${currentSourceRef.current?.key || ''}`,
|
||||||
|
poster: currentChannel.logo,
|
||||||
|
volume: 0.7,
|
||||||
|
isLive: true, // 设置为直播模式
|
||||||
|
muted: false,
|
||||||
|
autoplay: true,
|
||||||
|
pip: true,
|
||||||
|
autoSize: false,
|
||||||
|
autoMini: false,
|
||||||
|
screenshot: false,
|
||||||
|
setting: false,
|
||||||
|
loop: true,
|
||||||
|
flip: false,
|
||||||
|
playbackRate: false,
|
||||||
|
aspectRatio: false,
|
||||||
|
fullscreen: true,
|
||||||
|
fullscreenWeb: true,
|
||||||
|
subtitleOffset: false,
|
||||||
|
miniProgressBar: false,
|
||||||
|
mutex: true,
|
||||||
|
playsInline: true,
|
||||||
|
autoPlayback: false,
|
||||||
|
airplay: true,
|
||||||
|
theme: '#22c55e',
|
||||||
|
lang: 'zh-cn',
|
||||||
|
hotkey: false,
|
||||||
|
fastForward: false, // 直播不需要快进
|
||||||
|
autoOrientation: true,
|
||||||
|
lock: true,
|
||||||
|
moreVideoAttr: {
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
preload: 'metadata',
|
||||||
|
},
|
||||||
|
type: type,
|
||||||
|
customType: customType,
|
||||||
|
icons: {
|
||||||
|
loading:
|
||||||
|
'<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCIgdmlld0JveD0iMCAwIDUwIDUwIj48cGF0aCBkPSJNMjUuMjUxIDYuNDYxYy0xMC4zMTggMC0xOC42ODMgOC4zNjUtMTguNjgzIDE4LjY4M2g0LjA2OGMwLTguMDcgNi41NDUtMTQuNjE1IDE0LjYxNS0xNC42MTVWNi40NjF6IiBmaWxsPSIjMDA5Njg4Ij48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIGF0dHJpYnV0ZVR5cGU9IlhNTCIgZHVyPSIxcyIgZnJvbT0iMCAyNSAyNSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHRvPSIzNjAgMjUgMjUiIHR5cGU9InJvdGF0ZSIvPjwvcGF0aD48L3N2Zz4=">',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听播放器事件
|
||||||
|
artPlayerRef.current.on('ready', () => {
|
||||||
|
setError(null);
|
||||||
|
setIsVideoLoading(false);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
artPlayerRef.current.on('loadstart', () => {
|
||||||
|
setIsVideoLoading(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
artPlayerRef.current.on('loadeddata', () => {
|
||||||
|
setIsVideoLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
artPlayerRef.current.on('canplay', () => {
|
||||||
|
setIsVideoLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
artPlayerRef.current.on('waiting', () => {
|
||||||
|
setIsVideoLoading(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
artPlayerRef.current.on('error', (err: any) => {
|
||||||
|
console.error('播放器错误:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (artPlayerRef.current?.video) {
|
||||||
|
const finalUrl = `/api/proxy/m3u8?url=${encodeURIComponent(videoUrl)}`;
|
||||||
|
ensureVideoSource(
|
||||||
|
artPlayerRef.current.video as HTMLVideoElement,
|
||||||
|
finalUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('创建播放器失败:', err);
|
||||||
|
// 不设置错误,只记录日志
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preload();
|
||||||
}, [Artplayer, Hls, videoUrl, currentChannel, loading]);
|
}, [Artplayer, Hls, videoUrl, currentChannel, loading]);
|
||||||
|
|
||||||
// 清理播放器资源
|
// 清理播放器资源
|
||||||
|
|||||||
Reference in New Issue
Block a user