douyinweblive

This commit is contained in:
mtvpls
2026-01-20 20:31:13 +08:00
parent 30a89d878d
commit c354ec8168
4 changed files with 92 additions and 4 deletions

View File

@@ -10831,6 +10831,7 @@ const WebLiveConfig = ({
>
<option value='huya'></option>
<option value='bilibili'></option>
<option value='douyin'></option>
</select>
<input
type='text'
@@ -10881,6 +10882,7 @@ const WebLiveConfig = ({
>
<option value='huya'></option>
<option value='bilibili'></option>
<option value='douyin'></option>
</select>
</div>
<div>
@@ -10922,9 +10924,9 @@ const WebLiveConfig = ({
<tr key={source.key} className='hover:bg-gray-50 dark:hover:bg-gray-800'>
<td className='px-3 sm:px-6 py-4 text-sm text-gray-900 dark:text-gray-100'>
<div>{source.name}</div>
<div className='sm:hidden text-xs text-gray-500 dark:text-gray-400 mt-1'>{source.platform === 'huya' ? '虎牙' : source.platform === 'bilibili' ? '哔哩哔哩' : source.platform} · {source.roomId}</div>
<div className='sm:hidden text-xs text-gray-500 dark:text-gray-400 mt-1'>{source.platform === 'huya' ? '虎牙' : source.platform === 'bilibili' ? '哔哩哔哩' : source.platform === 'douyin' ? '抖音' : source.platform} · {source.roomId}</div>
</td>
<td className='hidden sm:table-cell px-6 py-4 text-sm text-gray-900 dark:text-gray-100'>{source.platform === 'huya' ? '虎牙' : source.platform === 'bilibili' ? '哔哩哔哩' : source.platform}</td>
<td className='hidden sm:table-cell px-6 py-4 text-sm text-gray-900 dark:text-gray-100'>{source.platform === 'huya' ? '虎牙' : source.platform === 'bilibili' ? '哔哩哔哩' : source.platform === 'douyin' ? '抖音' : source.platform}</td>
<td className='hidden sm:table-cell px-6 py-4 text-sm text-gray-900 dark:text-gray-100'>{source.roomId}</td>
<td className='px-3 sm:px-6 py-4 whitespace-nowrap'>
<span className={`px-2 py-1 text-xs rounded-full whitespace-nowrap ${!source.disabled ? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300' : 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'}`}>

View File

@@ -49,6 +49,8 @@ export async function GET(request: NextRequest) {
let referer = 'https://www.huya.com/';
if (url.includes('bilivideo.com') || url.includes('bilibili.com')) {
referer = 'https://live.bilibili.com/';
} else if (url.includes('douyin.com') || url.includes('douyincdn.com')) {
referer = 'https://live.douyin.com/';
}
const streamRes = await fetch(url, {

View File

@@ -97,6 +97,75 @@ async function getBilibiliStream(roomId: string) {
return m3u8Url;
}
async function getDouyinStream(roomId: string) {
const cookies = 'ttwid=1%7C2iDIYVmjzMcpZ20fcaFde0VghXAA3NaNXE_SLR68IyE%7C1761045455%7Cab35197d5cfb21df6cbb2fa7ef1c9262206b062c315b9d04da746d0b37dfbc7d';
const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36',
'Referer': 'https://live.douyin.com/',
'Cookie': cookies
};
// 构建API参数
const params = new URLSearchParams({
aid: '6383',
app_name: 'douyin_web',
live_id: '1',
device_platform: 'web',
language: 'zh-CN',
browser_language: 'zh-CN',
browser_platform: 'Win32',
browser_name: 'Chrome',
browser_version: '116.0.0.0',
web_rid: roomId,
msToken: ''
});
const apiUrl = `https://live.douyin.com/webcast/room/web/enter/?${params.toString()}`;
const response = await fetch(apiUrl, { headers });
const jsonData = await response.json();
if (!jsonData.data || !jsonData.data.data) {
throw new Error('获取直播间信息失败');
}
const roomData = jsonData.data.data[0];
const status = roomData.status;
if (status !== 2) {
throw new Error('直播未开启');
}
const streamUrl = roomData.stream_url;
if (!streamUrl) {
throw new Error('未找到流地址');
}
// 获取m3u8地址
const hlsPullUrlMap = streamUrl.hls_pull_url_map;
if (!hlsPullUrlMap) {
throw new Error('未找到m3u8地址');
}
// 尝试获取原画质,如果没有则获取第一个可用的
let m3u8Url = hlsPullUrlMap.ORIGIN || hlsPullUrlMap.FULL_HD1 || hlsPullUrlMap.HD1 || hlsPullUrlMap.SD1 || hlsPullUrlMap.SD2;
if (!m3u8Url) {
// 如果上述都没有,获取第一个可用的
const urls = Object.values(hlsPullUrlMap);
if (urls.length > 0) {
m3u8Url = urls[0] as string;
}
}
if (!m3u8Url) {
throw new Error('未找到可用的m3u8地址');
}
return m3u8Url;
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
@@ -148,6 +217,16 @@ export async function GET(request: NextRequest) {
});
}
if (platform === 'douyin') {
const streamUrl = await getDouyinStream(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(

View File

@@ -141,6 +141,9 @@ export default function WebLivePage() {
if (source.platform === 'bilibili') {
return `https://live.bilibili.com/${source.roomId}`;
}
if (source.platform === 'douyin') {
return `https://live.douyin.com/${source.roomId}`;
}
return '';
};
@@ -502,7 +505,7 @@ export default function WebLivePage() {
<div className='flex items-center gap-2'>
<span className='text-xs text-green-700 dark:text-green-300'>:</span>
<span className='text-sm font-medium text-green-800 dark:text-green-200'>
{selectedPlatform === 'huya' ? '虎牙' : selectedPlatform === 'bilibili' ? '哔哩哔哩' : selectedPlatform}
{selectedPlatform === 'huya' ? '虎牙' : selectedPlatform === 'bilibili' ? '哔哩哔哩' : selectedPlatform === 'douyin' ? '抖音' : selectedPlatform}
</span>
</div>
<button
@@ -562,13 +565,15 @@ export default function WebLivePage() {
<img src='https://hd.huya.com/favicon.ico' alt='虎牙' className='w-8 h-8' />
) : platform === 'bilibili' ? (
<img src='https://www.bilibili.com/favicon.ico' alt='哔哩哔哩' className='w-8 h-8' />
) : platform === 'douyin' ? (
<img src='https://lf1-cdn-tos.bytegoofy.com/goofy/ies/douyin_web/public/favicon.ico' alt='抖音' className='w-8 h-8' />
) : (
<Radio className='w-6 h-6 text-gray-500' />
)}
</div>
<div className='flex-1 min-w-0 text-left'>
<div className='text-sm font-medium text-gray-900 dark:text-gray-100 truncate'>
{platform === 'huya' ? '虎牙' : platform === 'bilibili' ? '哔哩哔哩' : platform}
{platform === 'huya' ? '虎牙' : platform === 'bilibili' ? '哔哩哔哩' : platform === 'douyin' ? '抖音' : platform}
</div>
<div className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
{sources.filter(s => s.platform === platform).length}