live更改为动态导入
This commit is contained in:
3151
pnpm-lock.yaml
generated
3151
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Artplayer from 'artplayer';
|
|
||||||
import Hls from 'hls.js';
|
|
||||||
import { Heart, Radio, Tv } from 'lucide-react';
|
import { Heart, Radio, Tv } from 'lucide-react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useLiveSync } from '@/hooks/useLiveSync';
|
import { useLiveSync } from '@/hooks/useLiveSync';
|
||||||
|
|
||||||
@@ -30,6 +28,10 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 动态导入浏览器专用库
|
||||||
|
let Artplayer: any = null;
|
||||||
|
let Hls: any = null;
|
||||||
|
|
||||||
// 直播频道接口
|
// 直播频道接口
|
||||||
interface LiveChannel {
|
interface LiveChannel {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -53,6 +55,14 @@ interface LiveSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function LivePageClient() {
|
function LivePageClient() {
|
||||||
|
// 动态加载浏览器专用库
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
import('artplayer').then(mod => { Artplayer = mod.default; });
|
||||||
|
import('hls.js').then(mod => { Hls = mod.default; });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// 状态变量(State)
|
// 状态变量(State)
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -135,6 +145,7 @@ function LivePageClient() {
|
|||||||
currentChannelUrl: currentChannel?.url || '',
|
currentChannelUrl: currentChannel?.url || '',
|
||||||
onChannelChange: (channelId, channelUrl) => {
|
onChannelChange: (channelId, channelUrl) => {
|
||||||
// 房员接收到频道切换指令
|
// 房员接收到频道切换指令
|
||||||
|
if (!currentChannels || !Array.isArray(currentChannels)) return;
|
||||||
const channel = currentChannels.find(c => c.id === channelId);
|
const channel = currentChannels.find(c => c.id === channelId);
|
||||||
if (channel) {
|
if (channel) {
|
||||||
handleChannelChange(channel);
|
handleChannelChange(channel);
|
||||||
@@ -801,7 +812,11 @@ function LivePageClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 动态导入anime4k-webgpu
|
// 动态导入anime4k-webgpu
|
||||||
const { render: anime4kRender, ModeA, ModeB, ModeC, ModeAA, ModeBB, ModeCA } = await import('anime4k-webgpu');
|
const { render: anime4kRender, ModeA, ModeB, ModeC, ModeAA, ModeBB, ModeCA } = await import(
|
||||||
|
/* webpackChunkName: "anime4k-webgpu" */
|
||||||
|
/* webpackMode: "lazy" */
|
||||||
|
'anime4k-webgpu'
|
||||||
|
);
|
||||||
|
|
||||||
let ModeClass: any;
|
let ModeClass: any;
|
||||||
const modeName = anime4kModeRef.current;
|
const modeName = anime4kModeRef.current;
|
||||||
@@ -1089,6 +1104,8 @@ function LivePageClient() {
|
|||||||
|
|
||||||
// 过滤频道(根据分组和搜索关键词)
|
// 过滤频道(根据分组和搜索关键词)
|
||||||
const filterChannels = (group: string, keyword: string) => {
|
const filterChannels = (group: string, keyword: string) => {
|
||||||
|
if (!currentChannels || !Array.isArray(currentChannels)) return [];
|
||||||
|
|
||||||
let filtered = currentChannels.filter(channel => channel.group === group);
|
let filtered = currentChannels.filter(channel => channel.group === group);
|
||||||
|
|
||||||
// 如果有搜索关键词,进一步过滤
|
// 如果有搜索关键词,进一步过滤
|
||||||
@@ -1136,7 +1153,7 @@ function LivePageClient() {
|
|||||||
let filtered = filterChannels(selectedGroup, keyword);
|
let filtered = filterChannels(selectedGroup, keyword);
|
||||||
|
|
||||||
// 如果当前分组没有匹配的频道,且有搜索关键词,轮询所有分组
|
// 如果当前分组没有匹配的频道,且有搜索关键词,轮询所有分组
|
||||||
if (filtered.length === 0 && keyword.trim()) {
|
if (filtered.length === 0 && keyword.trim() && groupedChannels) {
|
||||||
const groups = Object.keys(groupedChannels);
|
const groups = Object.keys(groupedChannels);
|
||||||
|
|
||||||
// 轮询所有分组,找到第一个有匹配频道的分组
|
// 轮询所有分组,找到第一个有匹配频道的分组
|
||||||
@@ -1271,13 +1288,13 @@ function LivePageClient() {
|
|||||||
|
|
||||||
// 当分组切换时,将激活的分组标签滚动到视口中间
|
// 当分组切换时,将激活的分组标签滚动到视口中间
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedGroup || !groupContainerRef.current) return;
|
if (!selectedGroup || !groupContainerRef.current || !groupedChannels) return;
|
||||||
|
|
||||||
const groupKeys = Object.keys(groupedChannels);
|
const groupKeys = Object.keys(groupedChannels);
|
||||||
const groupIndex = groupKeys.indexOf(selectedGroup);
|
const groupIndex = groupKeys.indexOf(selectedGroup);
|
||||||
if (groupIndex === -1) return;
|
if (groupIndex === -1) return;
|
||||||
|
|
||||||
const btn = groupButtonRefs.current[groupIndex];
|
const btn = groupButtonRefs.current?.[groupIndex];
|
||||||
const container = groupContainerRef.current;
|
const container = groupContainerRef.current;
|
||||||
if (btn && container) {
|
if (btn && container) {
|
||||||
// 手动计算滚动位置,只滚动分组标签容器
|
// 手动计算滚动位置,只滚动分组标签容器
|
||||||
@@ -1301,51 +1318,51 @@ function LivePageClient() {
|
|||||||
}
|
}
|
||||||
}, [selectedGroup, groupedChannels]);
|
}, [selectedGroup, groupedChannels]);
|
||||||
|
|
||||||
class CustomHlsJsLoader extends Hls.DefaultConfig.loader {
|
|
||||||
constructor(config: any) {
|
|
||||||
super(config);
|
|
||||||
const load = this.load.bind(this);
|
|
||||||
this.load = function (context: any, config: any, callbacks: any) {
|
|
||||||
// 所有的请求都带一个 source 参数
|
|
||||||
try {
|
|
||||||
const url = new URL(context.url);
|
|
||||||
url.searchParams.set('moontv-source', currentSourceRef.current?.key || '');
|
|
||||||
context.url = url.toString();
|
|
||||||
} catch (error) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
// 拦截manifest和level请求
|
|
||||||
if (
|
|
||||||
(context as any).type === 'manifest' ||
|
|
||||||
(context as any).type === 'level'
|
|
||||||
) {
|
|
||||||
// 判断是否浏览器直连
|
|
||||||
const isLiveDirectConnectStr = localStorage.getItem('liveDirectConnect');
|
|
||||||
const isLiveDirectConnect = isLiveDirectConnectStr === 'true';
|
|
||||||
if (isLiveDirectConnect) {
|
|
||||||
// 浏览器直连,使用 URL 对象处理参数
|
|
||||||
try {
|
|
||||||
const url = new URL(context.url);
|
|
||||||
url.searchParams.set('allowCORS', 'true');
|
|
||||||
context.url = url.toString();
|
|
||||||
} catch (error) {
|
|
||||||
// 如果 URL 解析失败,回退到字符串拼接
|
|
||||||
context.url = context.url + '&allowCORS=true';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 执行原始load方法
|
|
||||||
load(context, config, callbacks);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function m3u8Loader(video: HTMLVideoElement, url: string) {
|
function m3u8Loader(video: HTMLVideoElement, url: string) {
|
||||||
if (!Hls) {
|
if (!Hls) {
|
||||||
console.error('HLS.js 未加载');
|
console.error('HLS.js 未加载');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CustomHlsJsLoader extends Hls.DefaultConfig.loader {
|
||||||
|
constructor(config: any) {
|
||||||
|
super(config);
|
||||||
|
const load = this.load.bind(this);
|
||||||
|
this.load = function (context: any, config: any, callbacks: any) {
|
||||||
|
// 所有的请求都带一个 source 参数
|
||||||
|
try {
|
||||||
|
const url = new URL(context.url);
|
||||||
|
url.searchParams.set('moontv-source', currentSourceRef.current?.key || '');
|
||||||
|
context.url = url.toString();
|
||||||
|
} catch (error) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
// 拦截manifest和level请求
|
||||||
|
if (
|
||||||
|
(context as any).type === 'manifest' ||
|
||||||
|
(context as any).type === 'level'
|
||||||
|
) {
|
||||||
|
// 判断是否浏览器直连
|
||||||
|
const isLiveDirectConnectStr = localStorage.getItem('liveDirectConnect');
|
||||||
|
const isLiveDirectConnect = isLiveDirectConnectStr === 'true';
|
||||||
|
if (isLiveDirectConnect) {
|
||||||
|
// 浏览器直连,使用 URL 对象处理参数
|
||||||
|
try {
|
||||||
|
const url = new URL(context.url);
|
||||||
|
url.searchParams.set('allowCORS', 'true');
|
||||||
|
context.url = url.toString();
|
||||||
|
} catch (error) {
|
||||||
|
// 如果 URL 解析失败,回退到字符串拼接
|
||||||
|
context.url = context.url + '&allowCORS=true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 执行原始load方法
|
||||||
|
load(context, config, callbacks);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 清理之前的 HLS 实例
|
// 清理之前的 HLS 实例
|
||||||
if (video.hls) {
|
if (video.hls) {
|
||||||
try {
|
try {
|
||||||
@@ -1411,16 +1428,27 @@ function LivePageClient() {
|
|||||||
|
|
||||||
// precheck type
|
// precheck type
|
||||||
let type = 'm3u8';
|
let type = 'm3u8';
|
||||||
const precheckUrl = `/api/live/precheck?url=${encodeURIComponent(videoUrl)}&moontv-source=${currentSourceRef.current?.key || ''}`;
|
try {
|
||||||
const precheckResponse = await fetch(precheckUrl);
|
const precheckUrl = `/api/live/precheck?url=${encodeURIComponent(videoUrl)}&moontv-source=${currentSourceRef.current?.key || ''}`;
|
||||||
if (!precheckResponse.ok) {
|
const precheckResponse = await fetch(precheckUrl);
|
||||||
console.error('预检查失败:', precheckResponse.statusText);
|
if (!precheckResponse.ok) {
|
||||||
|
console.error('预检查失败:', precheckResponse.statusText);
|
||||||
|
setIsVideoLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const precheckResult = await precheckResponse.json();
|
||||||
|
if (precheckResult?.success && precheckResult?.type) {
|
||||||
|
type = precheckResult.type;
|
||||||
|
} else {
|
||||||
|
console.error('预检查返回无效结果:', precheckResult);
|
||||||
|
setIsVideoLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('预检查异常:', err);
|
||||||
|
setIsVideoLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const precheckResult = await precheckResponse.json();
|
|
||||||
if (precheckResult.success) {
|
|
||||||
type = precheckResult.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果不是 m3u8 类型,设置不支持的类型并返回
|
// 如果不是 m3u8 类型,设置不支持的类型并返回
|
||||||
if (type !== 'm3u8') {
|
if (type !== 'm3u8') {
|
||||||
@@ -2221,7 +2249,7 @@ function LivePageClient() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='flex gap-4 min-w-max'>
|
<div className='flex gap-4 min-w-max'>
|
||||||
{Object.keys(groupedChannels).map((group, index) => (
|
{groupedChannels && Object.keys(groupedChannels).map((group, index) => (
|
||||||
<button
|
<button
|
||||||
key={group}
|
key={group}
|
||||||
data-group={group}
|
data-group={group}
|
||||||
@@ -2253,7 +2281,7 @@ function LivePageClient() {
|
|||||||
|
|
||||||
{/* 频道列表 */}
|
{/* 频道列表 */}
|
||||||
<div ref={channelListRef} className='flex-1 overflow-y-auto space-y-2 pb-4'>
|
<div ref={channelListRef} className='flex-1 overflow-y-auto space-y-2 pb-4'>
|
||||||
{filteredChannels.length > 0 ? (
|
{filteredChannels?.length > 0 ? (
|
||||||
filteredChannels.map(channel => {
|
filteredChannels.map(channel => {
|
||||||
const isActive = channel.id === currentChannel?.id;
|
const isActive = channel.id === currentChannel?.id;
|
||||||
return (
|
return (
|
||||||
@@ -2331,7 +2359,7 @@ function LivePageClient() {
|
|||||||
{activeTab === 'sources' && (
|
{activeTab === 'sources' && (
|
||||||
<div className='flex flex-col h-full mt-4'>
|
<div className='flex flex-col h-full mt-4'>
|
||||||
<div className='flex-1 overflow-y-auto space-y-2 pb-20'>
|
<div className='flex-1 overflow-y-auto space-y-2 pb-20'>
|
||||||
{liveSources.length > 0 ? (
|
{liveSources?.length > 0 ? (
|
||||||
liveSources.map((source) => {
|
liveSources.map((source) => {
|
||||||
const isCurrentSource = source.key === currentSource?.key;
|
const isCurrentSource = source.key === currentSource?.key;
|
||||||
return (
|
return (
|
||||||
@@ -2469,9 +2497,5 @@ const FavoriteIcon = ({ filled }: { filled: boolean }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function LivePage() {
|
export default function LivePage() {
|
||||||
return (
|
return <LivePageClient />;
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
|
||||||
<LivePageClient />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user