/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-explicit-any, react-hooks/exhaustive-deps, no-console, @next/next/no-img-element */ 'use client'; import Artplayer from 'artplayer'; import Hls from 'hls.js'; import { Radio, Tv } from 'lucide-react'; import { Suspense, useEffect, useRef, useState } from 'react'; import { parseCustomTimeFormat } from '@/lib/time'; import { processImageUrl } from '@/lib/utils'; import EpgScrollableRow from '@/components/EpgScrollableRow'; import PageLayout from '@/components/PageLayout'; // 扩展 HTMLVideoElement 类型以支持 hls 属性 declare global { interface HTMLVideoElement { hls?: any; } } // 直播频道接口 interface LiveChannel { id: string; tvgId: string; name: string; logo: string; group: string; url: string; } // 直播源接口 interface LiveSource { key: string; name: string; url: string; // m3u 地址 ua?: string; epg?: string; // 节目单 from: 'config' | 'custom'; channelNumber?: number; disabled?: boolean; } function LivePageClient() { // ----------------------------------------------------------------------------- // 状态变量(State) // ----------------------------------------------------------------------------- const [loading, setLoading] = useState(true); const [loadingStage, setLoadingStage] = useState< 'loading' | 'fetching' | 'ready' >('loading'); const [loadingMessage, setLoadingMessage] = useState('正在加载直播源...'); const [error, setError] = useState(null); // 直播源相关 const [liveSources, setLiveSources] = useState([]); const [currentSource, setCurrentSource] = useState(null); const currentSourceRef = useRef(null); useEffect(() => { currentSourceRef.current = currentSource; }, [currentSource]); // 频道相关 const [currentChannels, setCurrentChannels] = useState([]); const [currentChannel, setCurrentChannel] = useState(null); // 播放器相关 const [videoUrl, setVideoUrl] = useState(''); const [isVideoLoading, setIsVideoLoading] = useState(false); // 切换直播源状态 const [isSwitchingSource, setIsSwitchingSource] = useState(false); // 分组相关 const [groupedChannels, setGroupedChannels] = useState<{ [key: string]: LiveChannel[] }>({}); const [selectedGroup, setSelectedGroup] = useState(''); // Tab 切换 const [activeTab, setActiveTab] = useState<'channels' | 'sources'>('channels'); // 频道列表收起状态 const [isChannelListCollapsed, setIsChannelListCollapsed] = useState(false); // 过滤后的频道列表 const [filteredChannels, setFilteredChannels] = useState([]); // 节目单信息 const [epgData, setEpgData] = useState<{ tvgId: string; source: string; epgUrl: string; programs: Array<{ start: string; end: string; title: string; }>; } | null>(null); // EPG 数据加载状态 const [isEpgLoading, setIsEpgLoading] = useState(false); // EPG数据清洗函数 - 去除重叠的节目,保留时间较短的,只显示今日节目 const cleanEpgData = (programs: Array<{ start: string; end: string; title: string }>) => { if (!programs || programs.length === 0) return programs; console.log(`开始清洗EPG数据,原始节目数量: ${programs.length}`); // 获取今日日期(只考虑年月日,忽略时间) const today = new Date(); const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate()); const todayEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1); // 首先过滤出今日的节目(包括跨天节目) const todayPrograms = programs.filter(program => { const programStart = parseCustomTimeFormat(program.start); const programEnd = parseCustomTimeFormat(program.end); // 获取节目的日期范围 const programStartDate = new Date(programStart.getFullYear(), programStart.getMonth(), programStart.getDate()); const programEndDate = new Date(programEnd.getFullYear(), programEnd.getMonth(), programEnd.getDate()); // 如果节目的开始时间或结束时间在今天,或者节目跨越今天,都算作今天的节目 return ( (programStartDate >= todayStart && programStartDate < todayEnd) || // 开始时间在今天 (programEndDate >= todayStart && programEndDate < todayEnd) || // 结束时间在今天 (programStartDate < todayStart && programEndDate >= todayEnd) // 节目跨越今天(跨天节目) ); }); console.log(`过滤今日节目后数量: ${todayPrograms.length}`); // 按开始时间排序 const sortedPrograms = [...todayPrograms].sort((a, b) => { const startA = parseCustomTimeFormat(a.start).getTime(); const startB = parseCustomTimeFormat(b.start).getTime(); return startA - startB; }); const cleanedPrograms: Array<{ start: string; end: string; title: string }> = []; let removedCount = 0; const dateFilteredCount = programs.length - todayPrograms.length; for (let i = 0; i < sortedPrograms.length; i++) { const currentProgram = sortedPrograms[i]; const currentStart = parseCustomTimeFormat(currentProgram.start); const currentEnd = parseCustomTimeFormat(currentProgram.end); // 检查是否与已添加的节目重叠 let hasOverlap = false; for (const existingProgram of cleanedPrograms) { const existingStart = parseCustomTimeFormat(existingProgram.start); const existingEnd = parseCustomTimeFormat(existingProgram.end); // 检查时间重叠(只考虑时间部分,忽略日期) const currentTime = currentStart.getHours() * 60 + currentStart.getMinutes(); const currentEndTime = currentEnd.getHours() * 60 + currentEnd.getMinutes(); const existingTime = existingStart.getHours() * 60 + existingStart.getMinutes(); const existingEndTime = existingEnd.getHours() * 60 + existingEnd.getMinutes(); if ( (currentTime >= existingTime && currentTime < existingEndTime) || // 当前节目开始时间在已存在节目时间段内 (currentEndTime > existingTime && currentEndTime <= existingEndTime) || // 当前节目结束时间在已存在节目时间段内 (currentTime <= existingTime && currentEndTime >= existingEndTime) // 当前节目完全包含已存在节目 ) { hasOverlap = true; break; } } // 如果没有重叠,则添加该节目 if (!hasOverlap) { cleanedPrograms.push(currentProgram); } else { // 如果有重叠,检查是否需要替换已存在的节目 for (let j = 0; j < cleanedPrograms.length; j++) { const existingProgram = cleanedPrograms[j]; const existingStart = parseCustomTimeFormat(existingProgram.start); const existingEnd = parseCustomTimeFormat(existingProgram.end); // 检查是否与当前节目重叠(只考虑时间部分) const currentTime = currentStart.getHours() * 60 + currentStart.getMinutes(); const currentEndTime = currentEnd.getHours() * 60 + currentEnd.getMinutes(); const existingTime = existingStart.getHours() * 60 + existingStart.getMinutes(); const existingEndTime = existingEnd.getHours() * 60 + existingEnd.getMinutes(); if ( (currentTime >= existingTime && currentTime < existingEndTime) || (currentEndTime > existingTime && currentEndTime <= existingEndTime) || (currentTime <= existingTime && currentEndTime >= existingEndTime) ) { // 计算节目时长 const currentDuration = currentEnd.getTime() - currentStart.getTime(); const existingDuration = existingEnd.getTime() - existingStart.getTime(); // 如果当前节目时间更短,则替换已存在的节目 if (currentDuration < existingDuration) { console.log(`替换重叠节目: "${existingProgram.title}" (${existingDuration}ms) -> "${currentProgram.title}" (${currentDuration}ms)`); cleanedPrograms[j] = currentProgram; } else { console.log(`跳过重叠节目: "${currentProgram.title}" (${currentDuration}ms),保留 "${existingProgram.title}" (${existingDuration}ms)`); removedCount++; } break; } } } } console.log(`EPG数据清洗完成,清洗后节目数量: ${cleanedPrograms.length},移除重叠节目: ${removedCount}个,过滤非今日节目: ${dateFilteredCount}个`); return cleanedPrograms; }; // 播放器引用 const artPlayerRef = useRef(null); const artRef = useRef(null); // 分组标签滚动相关 const groupContainerRef = useRef(null); const groupButtonRefs = useRef<(HTMLButtonElement | null)[]>([]); // ----------------------------------------------------------------------------- // 工具函数(Utils) // ----------------------------------------------------------------------------- // 获取直播源列表 const fetchLiveSources = async () => { try { setLoadingStage('fetching'); setLoadingMessage('正在获取直播源...'); // 获取 AdminConfig 中的直播源信息 const response = await fetch('/api/live/sources'); if (!response.ok) { throw new Error('获取直播源失败'); } const result = await response.json(); if (!result.success) { throw new Error(result.error || '获取直播源失败'); } const sources = result.data; setLiveSources(sources); if (sources.length > 0) { // 默认选中第一个源 const firstSource = sources[0]; setCurrentSource(firstSource); await fetchChannels(firstSource); } setLoadingStage('ready'); setLoadingMessage('✨ 准备就绪...'); setTimeout(() => { setLoading(false); }, 1000); } catch (err) { console.error('获取直播源失败:', err); // 不设置错误,而是显示空状态 setLiveSources([]); setLoading(false); } }; // 获取频道列表 const fetchChannels = async (source: LiveSource) => { try { setIsVideoLoading(true); // 从 cachedLiveChannels 获取频道信息 const response = await fetch(`/api/live/channels?source=${source.key}`); if (!response.ok) { throw new Error('获取频道列表失败'); } const result = await response.json(); if (!result.success) { throw new Error(result.error || '获取频道列表失败'); } const channelsData = result.data; if (!channelsData || channelsData.length === 0) { // 不抛出错误,而是设置空频道列表 setCurrentChannels([]); setGroupedChannels({}); setFilteredChannels([]); // 更新直播源的频道数为 0 setLiveSources(prevSources => prevSources.map(s => s.key === source.key ? { ...s, channelNumber: 0 } : s ) ); setIsVideoLoading(false); return; } // 转换频道数据格式 const channels: LiveChannel[] = channelsData.map((channel: any) => ({ id: channel.id, tvgId: channel.tvgId || channel.name, name: channel.name, logo: channel.logo, group: channel.group || '其他', url: channel.url })); setCurrentChannels(channels); // 更新直播源的频道数 setLiveSources(prevSources => prevSources.map(s => s.key === source.key ? { ...s, channelNumber: channels.length } : s ) ); // 默认选中第一个频道 if (channels.length > 0) { setCurrentChannel(channels[0]); setVideoUrl(channels[0].url); } // 按分组组织频道 const grouped = channels.reduce((acc, channel) => { const group = channel.group || '其他'; if (!acc[group]) { acc[group] = []; } acc[group].push(channel); return acc; }, {} as { [key: string]: LiveChannel[] }); setGroupedChannels(grouped); // 默认选中第一个分组 const firstGroup = Object.keys(grouped)[0] || ''; setSelectedGroup(firstGroup); setFilteredChannels(firstGroup ? grouped[firstGroup] : channels); setIsVideoLoading(false); } catch (err) { console.error('获取频道列表失败:', err); // 不设置错误,而是设置空频道列表 setCurrentChannels([]); setGroupedChannels({}); setFilteredChannels([]); // 更新直播源的频道数为 0 setLiveSources(prevSources => prevSources.map(s => s.key === source.key ? { ...s, channelNumber: 0 } : s ) ); setIsVideoLoading(false); } }; // 切换直播源 const handleSourceChange = async (source: LiveSource) => { try { // 设置切换状态,锁住频道切换器 setIsSwitchingSource(true); // 清空节目单信息 setEpgData(null); setCurrentSource(source); await fetchChannels(source); } catch (err) { console.error('切换直播源失败:', err); // 不设置错误,保持当前状态 } finally { // 切换完成,解锁频道切换器 setIsSwitchingSource(false); // 自动切换到频道 tab setActiveTab('channels'); } }; // 切换频道 const handleChannelChange = async (channel: LiveChannel) => { // 如果正在切换直播源,则禁用频道切换 if (isSwitchingSource) return; setCurrentChannel(channel); setVideoUrl(channel.url); // 获取节目单信息 if (channel.tvgId && currentSource) { try { setIsEpgLoading(true); // 开始加载 EPG 数据 const response = await fetch(`/api/live/epg?source=${currentSource.key}&tvgId=${channel.tvgId}`); if (response.ok) { const result = await response.json(); if (result.success) { console.log('节目单信息:', result.data); // 清洗EPG数据,去除重叠的节目 const cleanedData = { ...result.data, programs: cleanEpgData(result.data.programs) }; setEpgData(cleanedData); } } } catch (error) { console.error('获取节目单信息失败:', error); } finally { setIsEpgLoading(false); // 无论成功失败都结束加载状态 } } else { // 如果没有 tvgId 或 currentSource,清空 EPG 数据 setEpgData(null); setIsEpgLoading(false); } }; // 清理播放器资源的统一函数 const cleanupPlayer = () => { if (artPlayerRef.current) { try { // 销毁 HLS 实例 if (artPlayerRef.current.video && artPlayerRef.current.video.hls) { artPlayerRef.current.video.hls.destroy(); } // 销毁 ArtPlayer 实例 artPlayerRef.current.destroy(); artPlayerRef.current = null; } catch (err) { console.warn('清理播放器资源时出错:', err); artPlayerRef.current = null; } } }; // 确保视频源正确设置 const ensureVideoSource = (video: HTMLVideoElement | null, url: string) => { if (!video || !url) return; const sources = Array.from(video.getElementsByTagName('source')); const existed = sources.some((s) => s.src === url); if (!existed) { // 移除旧的 source,保持唯一 sources.forEach((s) => s.remove()); const sourceEl = document.createElement('source'); sourceEl.src = url; video.appendChild(sourceEl); } // 始终允许远程播放(AirPlay / Cast) video.disableRemotePlayback = false; // 如果曾经有禁用属性,移除之 if (video.hasAttribute('disableRemotePlayback')) { video.removeAttribute('disableRemotePlayback'); } }; // 切换分组 const handleGroupChange = (group: string) => { // 如果正在切换直播源,则禁用分组切换 if (isSwitchingSource) return; setSelectedGroup(group); const filtered = currentChannels.filter(channel => channel.group === group); setFilteredChannels(filtered); }; // 初始化 useEffect(() => { fetchLiveSources(); }, []); // 当分组切换时,将激活的分组标签滚动到视口中间 useEffect(() => { if (!selectedGroup || !groupContainerRef.current) return; const groupKeys = Object.keys(groupedChannels); const groupIndex = groupKeys.indexOf(selectedGroup); if (groupIndex === -1) return; const btn = groupButtonRefs.current[groupIndex]; const container = groupContainerRef.current; if (btn && container) { // 手动计算滚动位置,只滚动分组标签容器 const containerRect = container.getBoundingClientRect(); const btnRect = btn.getBoundingClientRect(); const scrollLeft = container.scrollLeft; // 计算按钮相对于容器的位置 const btnLeft = btnRect.left - containerRect.left + scrollLeft; const btnWidth = btnRect.width; const containerWidth = containerRect.width; // 计算目标滚动位置,使按钮居中 const targetScrollLeft = btnLeft - (containerWidth - btnWidth) / 2; // 平滑滚动到目标位置 container.scrollTo({ left: targetScrollLeft, behavior: 'smooth', }); } }, [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); }; } } // 播放器初始化 useEffect(() => { if ( !Artplayer || !Hls || !videoUrl || !artRef.current || !currentChannel ) { return; } console.log('视频URL:', videoUrl); // 销毁之前的播放器实例并创建新的 if (artPlayerRef.current) { cleanupPlayer(); } try { // 创建新的播放器实例 Artplayer.USE_RAF = true; artPlayerRef.current = new Artplayer({ container: artRef.current, url: videoUrl.toLowerCase().endsWith('.mp4') ? videoUrl : `/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: 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: '', }, }); // 监听播放器事件 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) { console.error('创建播放器失败:', err); // 不设置错误,只记录日志 } }, [Artplayer, Hls, videoUrl, currentChannel, loading]); // 清理播放器资源 useEffect(() => { return () => { cleanupPlayer(); }; }, []); // 全局快捷键处理 useEffect(() => { const handleKeyboardShortcuts = (e: KeyboardEvent) => { // 忽略输入框中的按键事件 if ( (e.target as HTMLElement).tagName === 'INPUT' || (e.target as HTMLElement).tagName === 'TEXTAREA' ) return; // 上箭头 = 音量+ if (e.key === 'ArrowUp') { if (artPlayerRef.current && artPlayerRef.current.volume < 1) { artPlayerRef.current.volume = Math.round((artPlayerRef.current.volume + 0.1) * 10) / 10; artPlayerRef.current.notice.show = `音量: ${Math.round( artPlayerRef.current.volume * 100 )}`; e.preventDefault(); } } // 下箭头 = 音量- if (e.key === 'ArrowDown') { if (artPlayerRef.current && artPlayerRef.current.volume > 0) { artPlayerRef.current.volume = Math.round((artPlayerRef.current.volume - 0.1) * 10) / 10; artPlayerRef.current.notice.show = `音量: ${Math.round( artPlayerRef.current.volume * 100 )}`; e.preventDefault(); } } // 空格 = 播放/暂停 if (e.key === ' ') { if (artPlayerRef.current) { artPlayerRef.current.toggle(); e.preventDefault(); } } // f 键 = 切换全屏 if (e.key === 'f' || e.key === 'F') { if (artPlayerRef.current) { artPlayerRef.current.fullscreen = !artPlayerRef.current.fullscreen; e.preventDefault(); } } }; document.addEventListener('keydown', handleKeyboardShortcuts); return () => { document.removeEventListener('keydown', handleKeyboardShortcuts); }; }, []); if (loading) { return (
{/* 动画直播图标 */}
📺
{/* 旋转光环 */}
{/* 浮动粒子效果 */}
{/* 进度指示器 */}
{/* 进度条 */}
{/* 加载消息 */}

{loadingMessage}

); } if (error) { return (
{/* 错误图标 */}
😵
{/* 脉冲效果 */}
{/* 错误信息 */}

哎呀,出现了一些问题

{error}

请检查网络连接或尝试刷新页面

{/* 操作按钮 */}
); } return (
{/* 第一行:页面标题 */}

{currentSource?.name} {currentSource && currentChannel && ( {` > ${currentChannel.name}`} )} {currentSource && !currentChannel && ( {` > ${currentSource.name}`} )}

{/* 第二行:播放器和频道列表 */}
{/* 折叠控制 - 仅在 lg 及以上屏幕显示 */}
{/* 播放器 */}
{/* 视频加载蒙层 */} {isVideoLoading && (
📺

🔄 IPTV 加载中...

)}
{/* 频道列表 */}
{/* 主要的 Tab 切换 */}
setActiveTab('channels')} className={`flex-1 py-3 px-6 text-center cursor-pointer transition-all duration-200 font-medium ${activeTab === 'channels' ? 'text-green-600 dark:text-green-400' : 'text-gray-700 hover:text-green-600 bg-black/5 dark:bg-white/5 dark:text-gray-300 dark:hover:text-green-400 hover:bg-black/3 dark:hover:bg-white/3' } `.trim()} > 频道
setActiveTab('sources')} className={`flex-1 py-3 px-6 text-center cursor-pointer transition-all duration-200 font-medium ${activeTab === 'sources' ? 'text-green-600 dark:text-green-400' : 'text-gray-700 hover:text-green-600 bg-black/5 dark:bg-white/5 dark:text-gray-300 dark:hover:text-green-400 hover:bg-black/3 dark:hover:bg-white/3' } `.trim()} > 直播源
{/* 频道 Tab 内容 */} {activeTab === 'channels' && ( <> {/* 分组标签 */}
{/* 切换状态提示 */} {isSwitchingSource && (
切换直播源中...
)}
{ // 鼠标进入分组标签区域时,添加滚轮事件监听 const container = groupContainerRef.current; if (container) { const handleWheel = (e: WheelEvent) => { if (container.scrollWidth > container.clientWidth) { e.preventDefault(); container.scrollLeft += e.deltaY; } }; container.addEventListener('wheel', handleWheel, { passive: false }); // 将事件处理器存储在容器上,以便后续移除 (container as any)._wheelHandler = handleWheel; } }} onMouseLeave={() => { // 鼠标离开分组标签区域时,移除滚轮事件监听 const container = groupContainerRef.current; if (container && (container as any)._wheelHandler) { container.removeEventListener('wheel', (container as any)._wheelHandler); delete (container as any)._wheelHandler; } }} >
{Object.keys(groupedChannels).map((group, index) => ( ))}
{/* 频道列表 */}
{filteredChannels.length > 0 ? ( filteredChannels.map(channel => { const isActive = channel.id === currentChannel?.id; return ( ); }) ) : (

暂无可用频道

请选择其他直播源或稍后再试

)}
)} {/* 直播源 Tab 内容 */} {activeTab === 'sources' && (
{liveSources.length > 0 ? ( liveSources.map((source) => { const isCurrentSource = source.key === currentSource?.key; return (
!isCurrentSource && handleSourceChange(source)} className={`flex items-start gap-3 px-2 py-3 rounded-lg transition-all select-none duration-200 relative ${isCurrentSource ? 'bg-green-500/10 dark:bg-green-500/20 border-green-500/30 border' : 'hover:bg-gray-200/50 dark:hover:bg-white/10 hover:scale-[1.02] cursor-pointer' }`.trim()} > {/* 图标 */}
{/* 信息 */}
{source.name}
{!source.channelNumber || source.channelNumber === 0 ? '-' : `${source.channelNumber} 个频道`}
{/* 当前标识 */} {isCurrentSource && (
)}
); }) ) : (

暂无可用直播源

请检查网络连接或联系管理员添加直播源

)}
)}
{/* 当前频道信息 */} {currentChannel && (
{/* 频道图标+名称 - 在小屏幕上占100%,大屏幕占20% */}
{currentChannel.logo ? ( {currentChannel.name} ) : ( )}

{currentChannel.name}

{currentSource?.name} {' > '} {currentChannel.group}

{/* EPG节目单 */}
)}
); } export default function LivePage() { return ( Loading...}> ); }