From 30a89d878d65bef2add2a77fd63127b0431cf641 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Tue, 20 Jan 2026 20:17:06 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BD=91=E7=BB=9C=E7=9B=B4=E6=92=AD=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0bilibili?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/page.tsx | 6 +- .../api/web-live/proxy/[filename]/route.ts | 69 ++++++++++++++- src/app/api/web-live/stream/route.ts | 83 +++++++++++++++++++ src/app/web-live/page.tsx | 58 +++++++++++-- 4 files changed, 205 insertions(+), 11 deletions(-) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 570ca4b..eeb970f 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -10830,6 +10830,7 @@ const WebLiveConfig = ({ className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' > + +
@@ -10920,9 +10922,9 @@ const WebLiveConfig = ({
{source.name}
-
{source.platform === 'huya' ? '虎牙' : source.platform} · {source.roomId}
+
{source.platform === 'huya' ? '虎牙' : source.platform === 'bilibili' ? '哔哩哔哩' : source.platform} · {source.roomId}
- {source.platform === 'huya' ? '虎牙' : source.platform} + {source.platform === 'huya' ? '虎牙' : source.platform === 'bilibili' ? '哔哩哔哩' : source.platform} {source.roomId} diff --git a/src/app/api/web-live/proxy/[filename]/route.ts b/src/app/api/web-live/proxy/[filename]/route.ts index 9bcce73..31c76ac 100644 --- a/src/app/api/web-live/proxy/[filename]/route.ts +++ b/src/app/api/web-live/proxy/[filename]/route.ts @@ -1,5 +1,41 @@ import { NextRequest, NextResponse } from 'next/server'; +function getBaseUrl(url: string): string { + const urlObj = new URL(url); + const pathParts = urlObj.pathname.split('/'); + pathParts.pop(); + return `${urlObj.protocol}//${urlObj.host}${pathParts.join('/')}`; +} + +function processM3u8Content(content: string, baseUrl: string): string { + const lines = content.split('\n'); + const processedLines = lines.map(line => { + const trimmedLine = line.trim(); + + // 跳过注释行和空行 + if (trimmedLine.startsWith('#') || trimmedLine === '') { + return line; + } + + // 如果已经是完整URL,不处理 + if (trimmedLine.startsWith('http://') || trimmedLine.startsWith('https://')) { + return line; + } + + // 处理相对路径 + if (trimmedLine.startsWith('/')) { + // 绝对路径(相对于域名) + const urlObj = new URL(baseUrl); + return `${urlObj.protocol}//${urlObj.host}${trimmedLine}`; + } else { + // 相对路径 + return `${baseUrl}/${trimmedLine}`; + } + }); + + return processedLines.join('\n'); +} + export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); @@ -9,10 +45,16 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '缺少URL参数' }, { status: 400 }); } + // 根据URL判断Referer + let referer = 'https://www.huya.com/'; + if (url.includes('bilivideo.com') || url.includes('bilibili.com')) { + referer = 'https://live.bilibili.com/'; + } + const streamRes = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', - 'Referer': 'https://www.huya.com/' + 'Referer': referer } }); @@ -20,9 +62,32 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '无法获取直播流' }, { status: 404 }); } + const contentType = streamRes.headers.get('Content-Type') || ''; + + // 检测是否为m3u8文件 + const isM3u8 = url.endsWith('.m3u8') || + contentType.includes('application/vnd.apple.mpegurl') || + contentType.includes('application/x-mpegURL'); + + if (isM3u8) { + // 读取m3u8内容 + const content = await streamRes.text(); + const baseUrl = getBaseUrl(url); + const processedContent = processM3u8Content(content, baseUrl); + + return new NextResponse(processedContent, { + headers: { + 'Content-Type': 'application/vnd.apple.mpegurl', + 'Cache-Control': 'no-cache', + 'Access-Control-Allow-Origin': '*' + } + }); + } + + // 非m3u8文件,直接返回流 return new NextResponse(streamRes.body, { headers: { - 'Content-Type': streamRes.headers.get('Content-Type') || 'application/octet-stream', + 'Content-Type': contentType || 'application/octet-stream', 'Cache-Control': 'no-cache', 'Access-Control-Allow-Origin': '*' } diff --git a/src/app/api/web-live/stream/route.ts b/src/app/api/web-live/stream/route.ts index 0432772..c202f8e 100644 --- a/src/app/api/web-live/stream/route.ts +++ b/src/app/api/web-live/stream/route.ts @@ -24,6 +24,79 @@ function getAntiCode(oldAntiCode: string, streamName: string): string { 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`; } +async function getBilibiliStream(roomId: string) { + const 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', + 'Referer': 'https://live.bilibili.com/' + }; + + // 获取房间初始化信息 + const roomInitRes = await fetch(`https://api.live.bilibili.com/room/v1/Room/room_init?id=${roomId}`, { + headers + }); + const roomInitData = await roomInitRes.json(); + + if (roomInitData.code !== 0) { + throw new Error(roomInitData.message || '获取房间信息失败'); + } + + const roomData = roomInitData.data; + const realRoomId = roomData.room_id; + const liveStatus = roomData.live_status; // 0=未开播, 1=直播中, 2=轮播 + + if (liveStatus !== 1) { + throw new Error('直播未开启'); + } + + // 获取播放地址 (原画质量 qn=10000) + const playInfoRes = await fetch( + `https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?room_id=${realRoomId}&protocol=0,1&format=0,1,2&codec=0,1&qn=10000&platform=web&ptype=8`, + { headers } + ); + const playInfoData = await playInfoRes.json(); + + if (playInfoData.code !== 0) { + throw new Error(playInfoData.message || '获取播放信息失败'); + } + + const playurl = playInfoData.data?.playurl_info?.playurl; + if (!playurl) { + throw new Error('未找到播放地址'); + } + + // 提取m3u8地址 + let m3u8Url = ''; + const streamList = playurl.stream || []; + + for (const stream of streamList) { + const formatList = stream.format || []; + for (const fmt of formatList) { + if (fmt.format_name === 'ts') { + const codecList = fmt.codec || []; + for (const codec of codecList) { + const urlInfoList = codec.url_info || []; + const baseUrl = codec.base_url || ''; + + if (urlInfoList.length > 0 && baseUrl) { + const host = urlInfoList[0].host || ''; + const extra = urlInfoList[0].extra || ''; + m3u8Url = `${host}${baseUrl}${extra}`; + break; + } + } + if (m3u8Url) break; + } + } + if (m3u8Url) break; + } + + if (!m3u8Url) { + throw new Error('未找到m3u8地址'); + } + + return m3u8Url; +} + export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); @@ -65,6 +138,16 @@ export async function GET(request: NextRequest) { }); } + if (platform === 'bilibili') { + const streamUrl = await getBilibiliStream(roomId); + const proxyUrl = `/api/web-live/proxy/proxy.m3u8?url=${encodeURIComponent(streamUrl)}`; + + return NextResponse.json({ + url: proxyUrl, + originalUrl: streamUrl + }); + } + return NextResponse.json({ error: '不支持的平台' }, { status: 400 }); } catch (error) { return NextResponse.json( diff --git a/src/app/web-live/page.tsx b/src/app/web-live/page.tsx index 50d5f0d..84d8a96 100644 --- a/src/app/web-live/page.tsx +++ b/src/app/web-live/page.tsx @@ -23,6 +23,7 @@ export default function WebLivePage() { const [isChannelListCollapsed, setIsChannelListCollapsed] = useState(false); const [isVideoLoading, setIsVideoLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); + const [selectedPlatform, setSelectedPlatform] = useState(null); useEffect(() => { const meta = document.createElement('meta'); @@ -137,11 +138,30 @@ export default function WebLivePage() { if (source.platform === 'huya') { return `https://huya.com/${source.roomId}`; } + if (source.platform === 'bilibili') { + return `https://live.bilibili.com/${source.roomId}`; + } return ''; }; const platforms = Array.from(new Set(sources.map(s => s.platform))); + // 根据选中的平台筛选房间 + const filteredSources = selectedPlatform + ? sources.filter(s => s.platform === selectedPlatform) + : sources; + + // 处理平台点击 + const handlePlatformClick = (platform: string) => { + setSelectedPlatform(platform); + setActiveTab('rooms'); + }; + + // 清除平台筛选 + const clearPlatformFilter = () => { + setSelectedPlatform(null); + }; + if (loading) { return ( @@ -477,8 +497,24 @@ export default function WebLivePage() { {activeTab === 'rooms' && (
- {sources.length > 0 ? ( - sources.map((source) => { + {selectedPlatform && ( +
+
+ 筛选平台: + + {selectedPlatform === 'huya' ? '虎牙' : selectedPlatform === 'bilibili' ? '哔哩哔哩' : selectedPlatform} + +
+ +
+ )} + {filteredSources.length > 0 ? ( + filteredSources.map((source) => { const isActive = source.key === currentSource?.key; return (
-

暂无可用房间

+

+ {selectedPlatform ? '该平台暂无可用房间' : '暂无可用房间'} +

)} @@ -514,23 +552,29 @@ export default function WebLivePage() {
{platforms.length > 0 ? ( platforms.map((platform) => ( -
+ )) ) : (