From 16e56dbb3387cf11adbc6d1b6586ac5d8948a4c9 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Tue, 20 Jan 2026 12:55:50 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BD=91=E7=BB=9C=E7=9B=B4=E6=92=AD=E6=BA=90?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/web-live/stream/route.ts | 52 ++++++++++++++++++++---- src/app/web-live/page.tsx | 61 ++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/src/app/api/web-live/stream/route.ts b/src/app/api/web-live/stream/route.ts index ecfa0a9..03a2fe0 100644 --- a/src/app/api/web-live/stream/route.ts +++ b/src/app/api/web-live/stream/route.ts @@ -1,4 +1,28 @@ import { NextRequest, NextResponse } from 'next/server'; +import crypto from 'crypto'; + +function getAntiCode(oldAntiCode: string, streamName: string): string { + const paramsT = 100; + const sdkVersion = 2403051612; + const t13 = Date.now(); + const sdkSid = t13; + const initUuid = (Math.floor(t13 % 10000000000 * 1000) + Math.floor(1000 * Math.random())) % 4294967295; + const uid = Math.floor(Math.random() * (1400009999999 - 1400000000000 + 1)) + 1400000000000; + const seqId = uid + sdkSid; + const targetUnixTime = Math.floor((t13 + 110624) / 1000); + const wsTime = targetUnixTime.toString(16).toLowerCase(); + + const urlQuery = new URLSearchParams(oldAntiCode); + const fm = urlQuery.get('fm'); + if (!fm) return oldAntiCode; + + const wsSecretPf = Buffer.from(decodeURIComponent(fm), 'base64').toString().split('_')[0]; + const wsSecretHash = crypto.createHash('md5').update(`${seqId}|${urlQuery.get('ctype')}|${paramsT}`).digest('hex'); + const wsSecret = `${wsSecretPf}_${uid}_${streamName}_${wsSecretHash}_${wsTime}`; + const wsSecretMd5 = crypto.createHash('md5').update(wsSecret).digest('hex'); + + return `wsSecret=${wsSecretMd5}&wsTime=${wsTime}&seqid=${seqId}&ctype=${urlQuery.get('ctype')}&ver=1&fs=${urlQuery.get('fs')}&uuid=${initUuid}&u=${uid}&t=${paramsT}&sv=${sdkVersion}&sdk_sid=${sdkSid}&codec=264`; +} export async function GET(request: NextRequest) { try { @@ -11,16 +35,30 @@ export async function GET(request: NextRequest) { } if (platform === 'huya') { - const res = await fetch(`https://mp.huya.com/cache.php?m=Live&do=profileRoom&roomid=${roomId}`); - const data = await res.json(); + const res = await fetch(`https://www.huya.com/${roomId}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + } + }); + const html = await res.text(); - if (data.status === 200 && data.data?.liveStatus === 'ON') { - const stream = data.data.stream; - const url = `${stream.flv.multiLine[0].url}/${stream.flv.streamName}.${stream.flv.sFlvUrlSuffix}?${stream.flv.sFlvAntiCode}`; - return NextResponse.json({ url }); + const match = html.match(/stream:\s*(\{"data".*?),"iWebDefaultBitRate"/); + if (!match) { + return NextResponse.json({ error: '未找到直播数据' }, { status: 404 }); } - return NextResponse.json({ error: '直播未开启' }, { status: 404 }); + const jsonData = JSON.parse(match[1] + '}'); + const streamInfo = jsonData.data?.[0]?.gameStreamInfoList?.[0]; + + if (!streamInfo) { + return NextResponse.json({ error: '直播未开启' }, { status: 404 }); + } + + const { sFlvUrl, sStreamName, sFlvUrlSuffix, sFlvAntiCode } = streamInfo; + const newAntiCode = getAntiCode(sFlvAntiCode, sStreamName); + const url = `${sFlvUrl}/${sStreamName}.${sFlvUrlSuffix}?${newAntiCode}`; + + return NextResponse.json({ url }); } return NextResponse.json({ error: '不支持的平台' }, { status: 400 }); diff --git a/src/app/web-live/page.tsx b/src/app/web-live/page.tsx index 126d3ef..d931286 100644 --- a/src/app/web-live/page.tsx +++ b/src/app/web-live/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react'; import PageLayout from '@/components/PageLayout'; import { Radio } from 'lucide-react'; +import Head from 'next/head'; let Artplayer: any = null; let Hls: any = null; @@ -19,14 +20,25 @@ export default function WebLivePage() { const [videoUrl, setVideoUrl] = useState(''); const [activeTab, setActiveTab] = useState<'rooms' | 'platforms'>('rooms'); const [isChannelListCollapsed, setIsChannelListCollapsed] = useState(false); + const [isVideoLoading, setIsVideoLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); useEffect(() => { + const meta = document.createElement('meta'); + meta.name = 'referrer'; + meta.content = 'no-referrer'; + document.head.appendChild(meta); + 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(); + + return () => { + document.head.removeChild(meta); + }; }, []); const fetchSources = async () => { @@ -61,6 +73,11 @@ export default function WebLivePage() { if (!flvjs) return; const flvPlayer = flvjs.createPlayer({ type: 'flv', url, isLive: true }); flvPlayer.attachMediaElement(video); + flvPlayer.on(flvjs.Events.ERROR, (errorType: string, errorDetail: string) => { + console.error('FLV.js error:', errorType, errorDetail); + setErrorMessage(`播放失败: ${errorType} - ${errorDetail}`); + setVideoUrl(''); + }); flvPlayer.load(); (video as any).flvPlayer = flvPlayer; } @@ -94,14 +111,22 @@ export default function WebLivePage() { const handleSourceClick = async (source: any) => { setCurrentSource(source); + setIsVideoLoading(true); + setErrorMessage(null); try { const res = await fetch(`/api/web-live/stream?platform=${source.platform}&roomId=${source.roomId}`); if (res.ok) { const data = await res.json(); setVideoUrl(data.url); + } else { + const data = await res.json(); + setErrorMessage(data.error || '获取直播流失败'); } } catch (err) { console.error('获取直播流失败:', err); + setErrorMessage(err instanceof Error ? err.message : '获取直播流失败'); + } finally { + setIsVideoLoading(false); } }; @@ -210,6 +235,42 @@ export default function WebLivePage() {
+ + {errorMessage && ( +
+
+
+
+
⚠️
+
+
+
+
+

获取直播流失败

+
+

{errorMessage}

+
+

请尝试其他房间

+
+
+
+ )} + + {isVideoLoading && ( +
+
+
+
+
📺
+
+
+
+
+

🔄 加载中...

+
+
+
+ )}