网络直播源切换提示

This commit is contained in:
mtvpls
2026-01-20 12:55:50 +08:00
parent 1b566550af
commit 16e56dbb33
2 changed files with 106 additions and 7 deletions

View File

@@ -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 });

View File

@@ -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<string | null>(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() {
<div className={`h-full transition-all duration-300 ease-in-out ${isChannelListCollapsed ? 'col-span-1' : 'md:col-span-3'}`}>
<div className='relative w-full h-[300px] lg:h-full'>
<div ref={artRef} className='bg-black w-full h-full rounded-xl overflow-hidden shadow-lg border border-white/0 dark:border-white/30'></div>
{errorMessage && (
<div className='absolute inset-0 bg-black/90 backdrop-blur-sm rounded-xl overflow-hidden shadow-lg border border-white/0 dark:border-white/30 flex items-center justify-center z-[600] transition-all duration-300'>
<div className='text-center max-w-md mx-auto px-6'>
<div className='relative mb-8'>
<div className='relative mx-auto w-24 h-24 bg-gradient-to-r from-orange-500 to-red-600 rounded-2xl shadow-2xl flex items-center justify-center transform hover:scale-105 transition-transform duration-300'>
<div className='text-white text-4xl'></div>
<div className='absolute -inset-2 bg-gradient-to-r from-orange-500 to-red-600 rounded-2xl opacity-20 animate-pulse'></div>
</div>
</div>
<div className='space-y-4'>
<h3 className='text-xl font-semibold text-white'></h3>
<div className='bg-orange-500/20 border border-orange-500/30 rounded-lg p-4'>
<p className='text-orange-300 font-medium'>{errorMessage}</p>
</div>
<p className='text-sm text-gray-300'></p>
</div>
</div>
</div>
)}
{isVideoLoading && (
<div className='absolute inset-0 bg-black/85 backdrop-blur-sm rounded-xl overflow-hidden shadow-lg border border-white/0 dark:border-white/30 flex items-center justify-center z-[500] transition-all duration-300'>
<div className='text-center max-w-md mx-auto px-6'>
<div className='relative mb-8'>
<div className='relative mx-auto w-24 h-24 bg-gradient-to-r from-green-500 to-emerald-600 rounded-2xl shadow-2xl flex items-center justify-center transform hover:scale-105 transition-transform duration-300'>
<div className='text-white text-4xl'>📺</div>
<div className='absolute -inset-2 bg-gradient-to-r from-green-500 to-emerald-600 rounded-2xl opacity-20 animate-spin'></div>
</div>
</div>
<div className='space-y-2'>
<p className='text-xl font-semibold text-white animate-pulse'>🔄 ...</p>
</div>
</div>
</div>
)}
</div>
</div>