电视直播代理控制权从客户端改为服务端
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user