From 1b566550af85d9596aa456ae19db59f97d331a1b Mon Sep 17 00:00:00 2001 From: mtvpls Date: Tue, 20 Jan 2026 12:29:17 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BD=91=E7=BB=9C=E7=9B=B4=E6=92=AD=E6=BA=90?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 21 ++++ src/app/web-live/page.tsx | 242 ++++++++++++++++++++++++++++++++++---- 3 files changed, 239 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index c8d455e..4f06a62 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "cheerio": "^1.1.2", "clsx": "^2.0.0", "crypto-js": "^4.2.0", + "flv.js": "^1.6.2", "framer-motion": "^12.18.1", "he": "^1.2.0", "hls.js": "^1.6.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ef9976..03ccd35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: crypto-js: specifier: ^4.2.0 version: 4.2.0 + flv.js: + specifier: ^1.6.2 + version: 1.6.2 framer-motion: specifier: ^12.18.1 version: 12.23.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3186,6 +3189,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -3455,6 +3461,9 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flv.js@1.6.2: + resolution: {integrity: sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==} + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -6158,6 +6167,9 @@ packages: webpack-cli: optional: true + webworkify-webpack@2.1.5: + resolution: {integrity: sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==} + whatwg-encoding@1.0.5: resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} @@ -10176,6 +10188,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es6-promise@4.2.8: {} + escalade@3.2.0: {} escape-string-regexp@2.0.0: {} @@ -10527,6 +10541,11 @@ snapshots: flatted@3.3.3: {} + flv.js@1.6.2: + dependencies: + es6-promise: 4.2.8 + webworkify-webpack: 2.1.5 + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -13960,6 +13979,8 @@ snapshots: - esbuild - uglify-js + webworkify-webpack@2.1.5: {} + whatwg-encoding@1.0.5: dependencies: iconv-lite: 0.4.24 diff --git a/src/app/web-live/page.tsx b/src/app/web-live/page.tsx index 1e8588e..126d3ef 100644 --- a/src/app/web-live/page.tsx +++ b/src/app/web-live/page.tsx @@ -2,35 +2,50 @@ import { useEffect, useRef, useState } from 'react'; import PageLayout from '@/components/PageLayout'; +import { Radio } from 'lucide-react'; let Artplayer: any = null; let Hls: any = null; +let flvjs: any = null; export default function WebLivePage() { const artRef = useRef(null); const artPlayerRef = useRef(null); + const [loading, setLoading] = useState(true); + const [loadingStage, setLoadingStage] = useState<'loading' | 'fetching' | 'ready'>('loading'); + const [loadingMessage, setLoadingMessage] = useState('正在加载直播源...'); const [sources, setSources] = useState([]); const [currentSource, setCurrentSource] = useState(null); const [videoUrl, setVideoUrl] = useState(''); - const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState<'rooms' | 'platforms'>('rooms'); + const [isChannelListCollapsed, setIsChannelListCollapsed] = useState(false); useEffect(() => { if (typeof window !== 'undefined') { import('artplayer').then(mod => { Artplayer = mod.default; }); import('hls.js').then(mod => { Hls = mod.default; }); + import('flv.js').then(mod => { flvjs = mod.default; }); } fetchSources(); }, []); const fetchSources = async () => { try { + setLoading(true); + setLoadingStage('loading'); + setLoadingMessage('正在加载直播源...'); const res = await fetch('/api/web-live/sources'); if (res.ok) { + setLoadingStage('fetching'); const data = await res.json(); setSources(data); + setLoadingStage('ready'); + await new Promise(resolve => setTimeout(resolve, 500)); } } catch (err) { console.error('获取直播源失败:', err); + } finally { + setLoading(false); } }; @@ -42,8 +57,16 @@ export default function WebLivePage() { (video as any).hls = hls; } + function flvLoader(video: HTMLVideoElement, url: string) { + if (!flvjs) return; + const flvPlayer = flvjs.createPlayer({ type: 'flv', url, isLive: true }); + flvPlayer.attachMediaElement(video); + flvPlayer.load(); + (video as any).flvPlayer = flvPlayer; + } + useEffect(() => { - if (!Artplayer || !Hls || !videoUrl || !artRef.current) return; + if (!Artplayer || !Hls || !flvjs || !videoUrl || !artRef.current) return; if (artPlayerRef.current) { artPlayerRef.current.destroy(); @@ -54,7 +77,10 @@ export default function WebLivePage() { url: videoUrl, isLive: true, autoplay: true, - customType: { m3u8: m3u8Loader }, + customType: { + m3u8: m3u8Loader, + flv: flvLoader + }, icons: { loading: '' } }); @@ -68,7 +94,6 @@ export default function WebLivePage() { const handleSourceClick = async (source: any) => { setCurrentSource(source); - setIsLoading(true); try { const res = await fetch(`/api/web-live/stream?platform=${source.platform}&roomId=${source.roomId}`); if (res.ok) { @@ -77,33 +102,200 @@ export default function WebLivePage() { } } catch (err) { console.error('获取直播流失败:', err); - } finally { - setIsLoading(false); } }; + const platforms = Array.from(new Set(sources.map(s => s.platform))); + + if (loading) { + return ( + +
+
+ {/* 动画直播图标 */} +
+
+
📺
+ {/* 旋转光环 */} +
+
+ + {/* 浮动粒子效果 */} +
+
+
+
+
+
+ + {/* 进度指示器 */} +
+
+
+
+
+
+ + {/* 进度条 */} +
+
+
+
+ + {/* 加载消息 */} +
+

+ {loadingMessage} +

+
+
+
+
+ ); + } + return ( -
-

网络直播

-
-
-
-

直播列表

- {sources.map((source) => ( - - ))} + + + + {isChannelListCollapsed ? '显示' : '隐藏'} + +
+ +
+ +
+
+
+
+
+
+ +
+
+
+
setActiveTab('rooms')} + className={`flex-1 py-3 px-6 text-center cursor-pointer transition-all duration-200 font-medium ${activeTab === 'rooms' ? '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'}`} + > + 房间 +
+
setActiveTab('platforms')} + className={`flex-1 py-3 px-6 text-center cursor-pointer transition-all duration-200 font-medium ${activeTab === 'platforms' ? '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'}`} + > + 平台 +
+
+ + {activeTab === 'rooms' && ( +
+ {sources.length > 0 ? ( + sources.map((source) => { + const isActive = source.key === currentSource?.key; + return ( + + ); + }) + ) : ( +
+
+ +
+

暂无可用房间

+
+ )} +
+ )} + + {activeTab === 'platforms' && ( +
+
+ {platforms.length > 0 ? ( + platforms.map((platform) => ( +
+
+ +
+
+
+ {platform === 'huya' ? '虎牙' : platform} +
+
+ {sources.filter(s => s.platform === platform).length} 个房间 +
+
+
+ )) + ) : ( +
+
+ +
+

暂无可用平台

+
+ )} +
+
+ )} +
+