电视直播代理控制权从客户端改为服务端

This commit is contained in:
mtvpls
2026-03-16 15:47:12 +08:00
parent 3bec268481
commit fe210c26cb
5 changed files with 86 additions and 42 deletions
+71
View File
@@ -380,6 +380,7 @@ interface LiveDataSource {
channelNumber?: number; channelNumber?: number;
disabled?: boolean; disabled?: boolean;
from: 'config' | 'custom'; from: 'config' | 'custom';
proxyMode?: boolean; // 代理模式开关
} }
// 自定义分类数据类型 // 自定义分类数据类型
@@ -10936,6 +10937,49 @@ const LiveSourceConfig = ({
}); });
}; };
const handleToggleProxyMode = (key: string) => {
withLoading(`toggleLiveProxyMode_${key}`, async () => {
// 乐观更新本地状态
setLiveSources((prev) =>
prev.map((s) =>
s.key === key ? { ...s, proxyMode: !s.proxyMode } : s
)
);
try {
const response = await fetch('/api/admin/live', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'toggle_proxy_mode',
key,
}),
});
if (!response.ok) {
throw new Error('切换代理模式失败');
}
// 成功后刷新配置
await refreshConfig();
} catch (error) {
// 失败时回滚本地状态
setLiveSources((prev) =>
prev.map((s) =>
s.key === key ? { ...s, proxyMode: !s.proxyMode } : s
)
);
showError(
error instanceof Error ? error.message : '切换代理模式失败',
showAlert
);
throw error;
}
}).catch(() => {
console.error('操作失败', 'toggle_proxy_mode', key);
});
};
const handleDelete = (key: string) => { const handleDelete = (key: string) => {
withLoading(`deleteLiveSource_${key}`, () => withLoading(`deleteLiveSource_${key}`, () =>
callLiveSourceApi({ action: 'delete', key }) callLiveSourceApi({ action: 'delete', key })
@@ -11112,6 +11156,30 @@ const LiveSourceConfig = ({
{!liveSource.disabled ? '启用中' : '已禁用'} {!liveSource.disabled ? '启用中' : '已禁用'}
</span> </span>
</td> </td>
<td className='px-6 py-4 whitespace-nowrap'>
<button
onClick={() => {
handleToggleProxyMode(liveSource.key);
}}
disabled={isLoading(`toggleLiveProxyMode_${liveSource.key}`)}
className={`relative inline-flex items-center h-6 w-11 rounded-full transition-colors ${
liveSource.proxyMode
? 'bg-blue-600 dark:bg-blue-500'
: 'bg-gray-200 dark:bg-gray-700'
} ${
isLoading(`toggleLiveProxyMode_${liveSource.key}`)
? 'opacity-50 cursor-not-allowed'
: 'cursor-pointer'
}`}
title={liveSource.proxyMode ? '代理模式已启用' : '代理模式已禁用'}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
liveSource.proxyMode ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</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
onClick={() => handleToggleEnable(liveSource.key)} onClick={() => handleToggleEnable(liveSource.key)}
@@ -11419,6 +11487,9 @@ const LiveSourceConfig = ({
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'> <th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
</th> </th>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
</th>
<th className='px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'> <th className='px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
</th> </th>
+9
View File
@@ -153,6 +153,15 @@ export async function POST(request: NextRequest) {
config.LiveConfig = sortedLiveConfig; config.LiveConfig = sortedLiveConfig;
break; break;
case 'toggle_proxy_mode':
// 切换代理模式
const toggleSource = config.LiveConfig.find((l) => l.key === key);
if (!toggleSource) {
return NextResponse.json({ error: '直播源不存在' }, { status: 404 });
}
toggleSource.proxyMode = !toggleSource.proxyMode;
break;
default: default:
return NextResponse.json({ error: '未知操作' }, { status: 400 }); return NextResponse.json({ error: '未知操作' }, { status: 400 });
} }
+5 -4
View File
@@ -53,6 +53,7 @@ interface LiveSource {
from: 'config' | 'custom'; from: 'config' | 'custom';
channelNumber?: number; channelNumber?: number;
disabled?: boolean; disabled?: boolean;
proxyMode?: boolean; // 代理模式开关
} }
function LivePageClient() { function LivePageClient() {
@@ -1344,10 +1345,10 @@ function LivePageClient() {
(context as any).type === 'manifest' || (context as any).type === 'manifest' ||
(context as any).type === 'level' (context as any).type === 'level'
) { ) {
// 判断是否浏览器直连 // 判断当前直播源是否启用代理模式
const isLiveDirectConnectStr = localStorage.getItem('liveDirectConnect'); const currentLiveSource = currentSourceRef.current;
const isLiveDirectConnect = isLiveDirectConnectStr === 'true'; const isDirectConnect = currentLiveSource?.proxyMode === false;
if (isLiveDirectConnect) { if (isDirectConnect) {
// 浏览器直连,使用 URL 对象处理参数 // 浏览器直连,使用 URL 对象处理参数
try { try {
const url = new URL(context.url); const url = new URL(context.url);
-38
View File
@@ -114,7 +114,6 @@ export const UserMenu: React.FC = () => {
const [enableOptimization, setEnableOptimization] = useState(true); const [enableOptimization, setEnableOptimization] = useState(true);
const [speedTestTimeout, setSpeedTestTimeout] = useState(4000); // 测速超时时间(毫秒) const [speedTestTimeout, setSpeedTestTimeout] = useState(4000); // 测速超时时间(毫秒)
const [fluidSearch, setFluidSearch] = useState(true); const [fluidSearch, setFluidSearch] = useState(true);
const [liveDirectConnect, setLiveDirectConnect] = useState(false);
const [tmdbBackdropDisabled, setTmdbBackdropDisabled] = useState(false); const [tmdbBackdropDisabled, setTmdbBackdropDisabled] = useState(false);
const [enableTrailers, setEnableTrailers] = useState(false); const [enableTrailers, setEnableTrailers] = useState(false);
const [doubanDataSource, setDoubanDataSource] = useState('cmliussss-cdn-tencent'); const [doubanDataSource, setDoubanDataSource] = useState('cmliussss-cdn-tencent');
@@ -486,11 +485,6 @@ export const UserMenu: React.FC = () => {
setFluidSearch(defaultFluidSearch); setFluidSearch(defaultFluidSearch);
} }
const savedLiveDirectConnect = localStorage.getItem('liveDirectConnect');
if (savedLiveDirectConnect !== null) {
setLiveDirectConnect(JSON.parse(savedLiveDirectConnect));
}
const savedTmdbBackdropDisabled = localStorage.getItem('tmdb_backdrop_disabled'); const savedTmdbBackdropDisabled = localStorage.getItem('tmdb_backdrop_disabled');
if (savedTmdbBackdropDisabled !== null) { if (savedTmdbBackdropDisabled !== null) {
setTmdbBackdropDisabled(savedTmdbBackdropDisabled === 'true'); setTmdbBackdropDisabled(savedTmdbBackdropDisabled === 'true');
@@ -1040,13 +1034,6 @@ export const UserMenu: React.FC = () => {
} }
}; };
const handleLiveDirectConnectToggle = (value: boolean) => {
setLiveDirectConnect(value);
if (typeof window !== 'undefined') {
localStorage.setItem('liveDirectConnect', JSON.stringify(value));
}
};
const handleTmdbBackdropDisabledToggle = (value: boolean) => { const handleTmdbBackdropDisabledToggle = (value: boolean) => {
setTmdbBackdropDisabled(value); setTmdbBackdropDisabled(value);
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@@ -1255,7 +1242,6 @@ export const UserMenu: React.FC = () => {
setDefaultAggregateSearch(true); setDefaultAggregateSearch(true);
setEnableOptimization(true); setEnableOptimization(true);
setFluidSearch(defaultFluidSearch); setFluidSearch(defaultFluidSearch);
setLiveDirectConnect(false);
setTmdbBackdropDisabled(false); setTmdbBackdropDisabled(false);
setEnableTrailers(false); setEnableTrailers(false);
setDoubanProxyUrl(defaultDoubanProxy); setDoubanProxyUrl(defaultDoubanProxy);
@@ -2031,30 +2017,6 @@ export const UserMenu: React.FC = () => {
</label> </label>
</div> </div>
{/* 直播视频浏览器直连 */}
<div className='flex items-center justify-between'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
IPTV
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
IPTV Allow CORS
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={liveDirectConnect}
onChange={(e) => handleLiveDirectConnectToggle(e.target.checked)}
/>
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
</div>
</label>
</div>
{/* 禁用背景图渲染 */} {/* 禁用背景图渲染 */}
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<div> <div>
+1
View File
@@ -96,6 +96,7 @@ export interface AdminConfig {
from: 'config' | 'custom'; from: 'config' | 'custom';
channelNumber?: number; channelNumber?: number;
disabled?: boolean; disabled?: boolean;
proxyMode?: boolean; // 代理模式开关:启用后由服务器代理m3u8和ts分片
}[]; }[];
WebLiveConfig?: { WebLiveConfig?: {
key: string; key: string;