电视直播三种代理模式
This commit is contained in:
+24
-26
@@ -380,7 +380,7 @@ interface LiveDataSource {
|
|||||||
channelNumber?: number;
|
channelNumber?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
from: 'config' | 'custom';
|
from: 'config' | 'custom';
|
||||||
proxyMode?: boolean; // 代理模式开关
|
proxyMode?: 'full' | 'm3u8-only' | 'direct'; // 代理模式
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义分类数据类型
|
// 自定义分类数据类型
|
||||||
@@ -10937,12 +10937,15 @@ const LiveSourceConfig = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleProxyMode = (key: string) => {
|
const handleSetProxyMode = (key: string, mode: 'full' | 'm3u8-only' | 'direct') => {
|
||||||
withLoading(`toggleLiveProxyMode_${key}`, async () => {
|
withLoading(`setLiveProxyMode_${key}`, async () => {
|
||||||
|
// 保存旧值用于回滚
|
||||||
|
const oldMode = liveSources.find((s) => s.key === key)?.proxyMode;
|
||||||
|
|
||||||
// 乐观更新本地状态
|
// 乐观更新本地状态
|
||||||
setLiveSources((prev) =>
|
setLiveSources((prev) =>
|
||||||
prev.map((s) =>
|
prev.map((s) =>
|
||||||
s.key === key ? { ...s, proxyMode: !s.proxyMode } : s
|
s.key === key ? { ...s, proxyMode: mode } : s
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -10951,13 +10954,14 @@ const LiveSourceConfig = ({
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
action: 'toggle_proxy_mode',
|
action: 'set_proxy_mode',
|
||||||
key,
|
key,
|
||||||
|
proxyMode: mode,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('切换代理模式失败');
|
throw new Error('设置代理模式失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 成功后刷新配置
|
// 成功后刷新配置
|
||||||
@@ -10966,17 +10970,17 @@ const LiveSourceConfig = ({
|
|||||||
// 失败时回滚本地状态
|
// 失败时回滚本地状态
|
||||||
setLiveSources((prev) =>
|
setLiveSources((prev) =>
|
||||||
prev.map((s) =>
|
prev.map((s) =>
|
||||||
s.key === key ? { ...s, proxyMode: !s.proxyMode } : s
|
s.key === key ? { ...s, proxyMode: oldMode } : s
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
showError(
|
showError(
|
||||||
error instanceof Error ? error.message : '切换代理模式失败',
|
error instanceof Error ? error.message : '设置代理模式失败',
|
||||||
showAlert
|
showAlert
|
||||||
);
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
console.error('操作失败', 'toggle_proxy_mode', key);
|
console.error('操作失败', 'set_proxy_mode', key);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -11157,28 +11161,22 @@ const LiveSourceConfig = ({
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className='px-6 py-4 whitespace-nowrap'>
|
<td className='px-6 py-4 whitespace-nowrap'>
|
||||||
<button
|
<select
|
||||||
onClick={() => {
|
value={liveSource.proxyMode || 'full'}
|
||||||
handleToggleProxyMode(liveSource.key);
|
onChange={(e) => {
|
||||||
|
handleSetProxyMode(liveSource.key, e.target.value as 'full' | 'm3u8-only' | 'direct');
|
||||||
}}
|
}}
|
||||||
disabled={isLoading(`toggleLiveProxyMode_${liveSource.key}`)}
|
disabled={isLoading(`setLiveProxyMode_${liveSource.key}`)}
|
||||||
className={`relative inline-flex items-center h-6 w-11 rounded-full transition-colors ${
|
className={`px-2 py-1 text-xs rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 ${
|
||||||
liveSource.proxyMode
|
isLoading(`setLiveProxyMode_${liveSource.key}`)
|
||||||
? 'bg-blue-600 dark:bg-blue-500'
|
|
||||||
: 'bg-gray-200 dark:bg-gray-700'
|
|
||||||
} ${
|
|
||||||
isLoading(`toggleLiveProxyMode_${liveSource.key}`)
|
|
||||||
? 'opacity-50 cursor-not-allowed'
|
? 'opacity-50 cursor-not-allowed'
|
||||||
: 'cursor-pointer'
|
: 'cursor-pointer'
|
||||||
}`}
|
}`}
|
||||||
title={liveSource.proxyMode ? '代理模式已启用' : '代理模式已禁用'}
|
|
||||||
>
|
>
|
||||||
<span
|
<option value='full'>全量代理</option>
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
<option value='m3u8-only'>仅代理m3u8</option>
|
||||||
liveSource.proxyMode ? 'translate-x-6' : 'translate-x-1'
|
<option value='direct'>直连</option>
|
||||||
}`}
|
</select>
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
|
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { action, key, name, url, ua, epg } = body;
|
const { action, key, name, url, ua, epg, proxyMode } = body;
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return NextResponse.json({ error: '配置不存在' }, { status: 404 });
|
return NextResponse.json({ error: '配置不存在' }, { status: 404 });
|
||||||
@@ -153,13 +153,16 @@ export async function POST(request: NextRequest) {
|
|||||||
config.LiveConfig = sortedLiveConfig;
|
config.LiveConfig = sortedLiveConfig;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'toggle_proxy_mode':
|
case 'set_proxy_mode':
|
||||||
// 切换代理模式
|
// 设置代理模式
|
||||||
const toggleSource = config.LiveConfig.find((l) => l.key === key);
|
const setProxySource = config.LiveConfig.find((l) => l.key === key);
|
||||||
if (!toggleSource) {
|
if (!setProxySource) {
|
||||||
return NextResponse.json({ error: '直播源不存在' }, { status: 404 });
|
return NextResponse.json({ error: '直播源不存在' }, { status: 404 });
|
||||||
}
|
}
|
||||||
toggleSource.proxyMode = !toggleSource.proxyMode;
|
if (!proxyMode || !['full', 'm3u8-only', 'direct'].includes(proxyMode)) {
|
||||||
|
return NextResponse.json({ error: '无效的代理模式' }, { status: 400 });
|
||||||
|
}
|
||||||
|
setProxySource.proxyMode = proxyMode as 'full' | 'm3u8-only' | 'direct';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -126,12 +126,12 @@ function rewriteM3U8Content(content: string, baseUrl: string, req: Request, allo
|
|||||||
|
|
||||||
// 处理 EXT-X-MAP 标签中的 URI
|
// 处理 EXT-X-MAP 标签中的 URI
|
||||||
if (line.startsWith('#EXT-X-MAP:')) {
|
if (line.startsWith('#EXT-X-MAP:')) {
|
||||||
line = rewriteMapUri(line, baseUrl, proxyBase);
|
line = rewriteMapUri(line, baseUrl, proxyBase, allowCORS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 EXT-X-KEY 标签中的 URI
|
// 处理 EXT-X-KEY 标签中的 URI
|
||||||
if (line.startsWith('#EXT-X-KEY:')) {
|
if (line.startsWith('#EXT-X-KEY:')) {
|
||||||
line = rewriteKeyUri(line, baseUrl, proxyBase);
|
line = rewriteKeyUri(line, baseUrl, proxyBase, allowCORS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理嵌套的 M3U8 文件 (EXT-X-STREAM-INF)
|
// 处理嵌套的 M3U8 文件 (EXT-X-STREAM-INF)
|
||||||
@@ -158,23 +158,23 @@ function rewriteM3U8Content(content: string, baseUrl: string, req: Request, allo
|
|||||||
return rewrittenLines.join('\n');
|
return rewrittenLines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function rewriteMapUri(line: string, baseUrl: string, proxyBase: string) {
|
function rewriteMapUri(line: string, baseUrl: string, proxyBase: string, allowCORS: boolean) {
|
||||||
const uriMatch = line.match(/URI="([^"]+)"/);
|
const uriMatch = line.match(/URI="([^"]+)"/);
|
||||||
if (uriMatch) {
|
if (uriMatch) {
|
||||||
const originalUri = uriMatch[1];
|
const originalUri = uriMatch[1];
|
||||||
const resolvedUrl = resolveUrl(baseUrl, originalUri);
|
const resolvedUrl = resolveUrl(baseUrl, originalUri);
|
||||||
const proxyUrl = `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}`;
|
const proxyUrl = allowCORS ? resolvedUrl : `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}`;
|
||||||
return line.replace(uriMatch[0], `URI="${proxyUrl}"`);
|
return line.replace(uriMatch[0], `URI="${proxyUrl}"`);
|
||||||
}
|
}
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rewriteKeyUri(line: string, baseUrl: string, proxyBase: string) {
|
function rewriteKeyUri(line: string, baseUrl: string, proxyBase: string, allowCORS: boolean) {
|
||||||
const uriMatch = line.match(/URI="([^"]+)"/);
|
const uriMatch = line.match(/URI="([^"]+)"/);
|
||||||
if (uriMatch) {
|
if (uriMatch) {
|
||||||
const originalUri = uriMatch[1];
|
const originalUri = uriMatch[1];
|
||||||
const resolvedUrl = resolveUrl(baseUrl, originalUri);
|
const resolvedUrl = resolveUrl(baseUrl, originalUri);
|
||||||
const proxyUrl = `${proxyBase}/key?url=${encodeURIComponent(resolvedUrl)}`;
|
const proxyUrl = allowCORS ? resolvedUrl : `${proxyBase}/key?url=${encodeURIComponent(resolvedUrl)}`;
|
||||||
return line.replace(uriMatch[0], `URI="${proxyUrl}"`);
|
return line.replace(uriMatch[0], `URI="${proxyUrl}"`);
|
||||||
}
|
}
|
||||||
return line;
|
return line;
|
||||||
|
|||||||
+92
-46
@@ -53,7 +53,7 @@ interface LiveSource {
|
|||||||
from: 'config' | 'custom';
|
from: 'config' | 'custom';
|
||||||
channelNumber?: number;
|
channelNumber?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
proxyMode?: boolean; // 代理模式开关
|
proxyMode?: 'full' | 'm3u8-only' | 'direct'; // 代理模式
|
||||||
}
|
}
|
||||||
|
|
||||||
function LivePageClient() {
|
function LivePageClient() {
|
||||||
@@ -296,6 +296,12 @@ function LivePageClient() {
|
|||||||
// 工具函数(Utils)
|
// 工具函数(Utils)
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 获取 logo URL(始终使用代理)
|
||||||
|
const getLogoUrl = (logoUrl: string, sourceKey: string) => {
|
||||||
|
if (!logoUrl) return '';
|
||||||
|
return `/api/proxy/logo?url=${encodeURIComponent(logoUrl)}&source=${sourceKey}`;
|
||||||
|
};
|
||||||
|
|
||||||
// 获取直播源列表
|
// 获取直播源列表
|
||||||
const fetchLiveSources = async () => {
|
const fetchLiveSources = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -436,7 +442,7 @@ function LivePageClient() {
|
|||||||
title: selectedChannel.name,
|
title: selectedChannel.name,
|
||||||
source_name: source.name,
|
source_name: source.name,
|
||||||
year: '',
|
year: '',
|
||||||
cover: `/api/proxy/logo?url=${encodeURIComponent(selectedChannel.logo)}&source=${source.key}`,
|
cover: getLogoUrl(selectedChannel.logo, source.key),
|
||||||
index: 1,
|
index: 1,
|
||||||
total_episodes: 1,
|
total_episodes: 1,
|
||||||
play_time: 0,
|
play_time: 0,
|
||||||
@@ -627,7 +633,7 @@ function LivePageClient() {
|
|||||||
title: channel.name,
|
title: channel.name,
|
||||||
source_name: currentSource.name,
|
source_name: currentSource.name,
|
||||||
year: '',
|
year: '',
|
||||||
cover: `/api/proxy/logo?url=${encodeURIComponent(channel.logo)}&source=${currentSource.key}`,
|
cover: getLogoUrl(channel.logo, currentSource.key),
|
||||||
index: 1,
|
index: 1,
|
||||||
total_episodes: 1,
|
total_episodes: 1,
|
||||||
play_time: 0,
|
play_time: 0,
|
||||||
@@ -1204,7 +1210,7 @@ function LivePageClient() {
|
|||||||
title: currentChannelRef.current.name,
|
title: currentChannelRef.current.name,
|
||||||
source_name: currentSourceRef.current.name,
|
source_name: currentSourceRef.current.name,
|
||||||
year: '',
|
year: '',
|
||||||
cover: `/api/proxy/logo?url=${encodeURIComponent(currentChannelRef.current.logo)}&source=${currentSourceRef.current.key}`,
|
cover: getLogoUrl(currentChannelRef.current.logo, currentSourceRef.current.key),
|
||||||
total_episodes: 1,
|
total_episodes: 1,
|
||||||
save_time: Date.now(),
|
save_time: Date.now(),
|
||||||
search_title: '',
|
search_title: '',
|
||||||
@@ -1332,32 +1338,54 @@ function LivePageClient() {
|
|||||||
super(config);
|
super(config);
|
||||||
const load = this.load.bind(this);
|
const load = this.load.bind(this);
|
||||||
this.load = function (context: any, config: any, callbacks: any) {
|
this.load = function (context: any, config: any, callbacks: any) {
|
||||||
// 所有的请求都带一个 source 参数
|
// 判断当前直播源的代理模式
|
||||||
try {
|
const currentLiveSource = currentSourceRef.current;
|
||||||
const url = new URL(context.url);
|
const proxyMode = currentLiveSource?.proxyMode || 'full';
|
||||||
url.searchParams.set('moontv-source', currentSourceRef.current?.key || '');
|
|
||||||
context.url = url.toString();
|
|
||||||
} catch (error) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
// 拦截manifest和level请求
|
// 拦截manifest和level请求
|
||||||
if (
|
if (
|
||||||
(context as any).type === 'manifest' ||
|
(context as any).type === 'manifest' ||
|
||||||
(context as any).type === 'level'
|
(context as any).type === 'level'
|
||||||
) {
|
) {
|
||||||
// 判断当前直播源是否启用代理模式
|
// manifest 请求处理
|
||||||
const currentLiveSource = currentSourceRef.current;
|
if ((context as any).type === 'manifest') {
|
||||||
const isDirectConnect = currentLiveSource?.proxyMode === false;
|
if (proxyMode === 'full') {
|
||||||
if (isDirectConnect) {
|
// 全量代理:添加 source 参数
|
||||||
// 浏览器直连,使用 URL 对象处理参数
|
try {
|
||||||
try {
|
const url = new URL(context.url);
|
||||||
const url = new URL(context.url);
|
url.searchParams.set('moontv-source', currentSourceRef.current?.key || '');
|
||||||
url.searchParams.set('allowCORS', 'true');
|
context.url = url.toString();
|
||||||
context.url = url.toString();
|
} catch (error) {
|
||||||
} catch (error) {
|
// ignore
|
||||||
// 如果 URL 解析失败,回退到字符串拼接
|
}
|
||||||
context.url = context.url + '&allowCORS=true';
|
} else if (proxyMode === 'm3u8-only') {
|
||||||
|
// 仅代理m3u8模式:添加 source 参数和 allowCORS 参数
|
||||||
|
try {
|
||||||
|
const url = new URL(context.url);
|
||||||
|
url.searchParams.set('moontv-source', currentSourceRef.current?.key || '');
|
||||||
|
url.searchParams.set('allowCORS', 'true');
|
||||||
|
context.url = url.toString();
|
||||||
|
} catch (error) {
|
||||||
|
context.url = context.url + '&allowCORS=true';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// direct 模式:直接使用原始 URL,不添加任何参数
|
||||||
|
}
|
||||||
|
|
||||||
|
// level 请求(ts 分片)处理
|
||||||
|
if ((context as any).type === 'level') {
|
||||||
|
if (proxyMode === 'full') {
|
||||||
|
// 全量代理:添加 source 参数
|
||||||
|
try {
|
||||||
|
const url = new URL(context.url);
|
||||||
|
url.searchParams.set('moontv-source', currentSourceRef.current?.key || '');
|
||||||
|
context.url = url.toString();
|
||||||
|
} catch (error) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// m3u8-only 模式:ts 分片 URL 已经被代理服务器重写为原始 URL,不需要添加参数
|
||||||
|
// direct 模式:ts 分片直接使用原始 URL,不添加任何参数
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 执行原始load方法
|
// 执行原始load方法
|
||||||
@@ -1464,26 +1492,34 @@ function LivePageClient() {
|
|||||||
|
|
||||||
// precheck type
|
// precheck type
|
||||||
let type = 'm3u8';
|
let type = 'm3u8';
|
||||||
try {
|
const proxyMode = currentSourceRef.current?.proxyMode || 'full';
|
||||||
const precheckUrl = `/api/live/precheck?url=${encodeURIComponent(videoUrl)}&moontv-source=${currentSourceRef.current?.key || ''}`;
|
|
||||||
const precheckResponse = await fetch(precheckUrl);
|
// 直连模式:跳过服务器预检查,直接使用 m3u8
|
||||||
if (!precheckResponse.ok) {
|
if (proxyMode === 'direct') {
|
||||||
console.error('预检查失败:', precheckResponse.statusText);
|
type = 'm3u8';
|
||||||
|
} else {
|
||||||
|
// 全量代理或仅代理m3u8:通过服务器预检查
|
||||||
|
try {
|
||||||
|
const precheckUrl = `/api/live/precheck?url=${encodeURIComponent(videoUrl)}&moontv-source=${currentSourceRef.current?.key || ''}`;
|
||||||
|
const precheckResponse = await fetch(precheckUrl);
|
||||||
|
if (!precheckResponse.ok) {
|
||||||
|
console.error('预检查失败:', precheckResponse.statusText);
|
||||||
|
setIsVideoLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const precheckResult = await precheckResponse.json();
|
||||||
|
if (precheckResult?.success && precheckResult?.type) {
|
||||||
|
type = precheckResult.type;
|
||||||
|
} else {
|
||||||
|
console.error('预检查返回无效结果:', precheckResult);
|
||||||
|
setIsVideoLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('预检查异常:', err);
|
||||||
setIsVideoLoading(false);
|
setIsVideoLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const precheckResult = await precheckResponse.json();
|
|
||||||
if (precheckResult?.success && precheckResult?.type) {
|
|
||||||
type = precheckResult.type;
|
|
||||||
} else {
|
|
||||||
console.error('预检查返回无效结果:', precheckResult);
|
|
||||||
setIsVideoLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('预检查异常:', err);
|
|
||||||
setIsVideoLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果不是 m3u8、flv 或 mp4 类型,设置不支持的类型并返回
|
// 如果不是 m3u8、flv 或 mp4 类型,设置不支持的类型并返回
|
||||||
@@ -1497,9 +1533,19 @@ function LivePageClient() {
|
|||||||
setUnsupportedType(null);
|
setUnsupportedType(null);
|
||||||
|
|
||||||
const customType = { m3u8: m3u8Loader, flv: flvLoader };
|
const customType = { m3u8: m3u8Loader, flv: flvLoader };
|
||||||
const targetUrl = (type === 'flv' || type === 'mp4')
|
|
||||||
? videoUrl
|
// 根据代理模式决定 URL
|
||||||
: `/api/proxy/m3u8?url=${encodeURIComponent(videoUrl)}&moontv-source=${currentSourceRef.current?.key || ''}`;
|
let targetUrl = videoUrl;
|
||||||
|
if (type === 'm3u8') {
|
||||||
|
if (proxyMode === 'direct') {
|
||||||
|
// 直连模式:直接使用原始 URL
|
||||||
|
targetUrl = videoUrl;
|
||||||
|
} else {
|
||||||
|
// 全量代理或仅代理m3u8:使用代理 URL
|
||||||
|
targetUrl = `/api/proxy/m3u8?url=${encodeURIComponent(videoUrl)}&moontv-source=${currentSourceRef.current?.key || ''}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建新的播放器实例
|
// 创建新的播放器实例
|
||||||
Artplayer.USE_RAF = true;
|
Artplayer.USE_RAF = true;
|
||||||
@@ -2339,7 +2385,7 @@ function LivePageClient() {
|
|||||||
<div className='w-10 h-10 bg-gray-300 dark:bg-gray-700 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden'>
|
<div className='w-10 h-10 bg-gray-300 dark:bg-gray-700 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden'>
|
||||||
{channel.logo ? (
|
{channel.logo ? (
|
||||||
<img
|
<img
|
||||||
src={`/api/proxy/logo?url=${encodeURIComponent(channel.logo)}&source=${currentSource?.key || ''}`}
|
src={getLogoUrl(channel.logo, currentSource?.key || '')}
|
||||||
alt={channel.name}
|
alt={channel.name}
|
||||||
className='w-full h-full rounded object-contain'
|
className='w-full h-full rounded object-contain'
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@@ -2463,7 +2509,7 @@ function LivePageClient() {
|
|||||||
<div className='w-20 h-20 bg-gray-300 dark:bg-gray-700 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden'>
|
<div className='w-20 h-20 bg-gray-300 dark:bg-gray-700 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden'>
|
||||||
{currentChannel.logo ? (
|
{currentChannel.logo ? (
|
||||||
<img
|
<img
|
||||||
src={`/api/proxy/logo?url=${encodeURIComponent(currentChannel.logo)}&source=${currentSource?.key || ''}`}
|
src={getLogoUrl(currentChannel.logo, currentSource?.key || '')}
|
||||||
alt={currentChannel.name}
|
alt={currentChannel.name}
|
||||||
className='w-full h-full rounded object-contain'
|
className='w-full h-full rounded object-contain'
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export interface AdminConfig {
|
|||||||
from: 'config' | 'custom';
|
from: 'config' | 'custom';
|
||||||
channelNumber?: number;
|
channelNumber?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
proxyMode?: boolean; // 代理模式开关:启用后由服务器代理m3u8和ts分片
|
proxyMode?: 'full' | 'm3u8-only' | 'direct'; // 代理模式:full=全量代理,m3u8-only=仅代理m3u8,direct=直连
|
||||||
}[];
|
}[];
|
||||||
WebLiveConfig?: {
|
WebLiveConfig?: {
|
||||||
key: string;
|
key: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user