diff --git a/README.md b/README.md index 3e44983..6aeedf7 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ - [配置文件](#配置文件) - [自动更新](#自动更新) - [环境变量](#环境变量) +- [外部观影室服务器部署](#外部观影室服务器部署) - [弹幕后端部署](#弹幕后端部署) - [超分功能说明](#超分功能说明) - [AndroidTV 使用](#androidtv-使用) @@ -251,7 +252,10 @@ dockge/komodo 等 docker compose UI 也有自动更新功能 | NEXT_PUBLIC_DANMAKU_CACHE_EXPIRE_MINUTES | 弹幕缓存失效时间(分钟数,设为 0 时不缓存) | 0 或正整数 | 4320(3天) | | ENABLE_TVBOX_SUBSCRIBE | 是否启用 TVBOX 订阅功能 | true/false | false | | TVBOX_SUBSCRIBE_TOKEN | TVBOX 订阅 API 访问 Token,如启用TVBOX功能必须设置该项 | 任意字符串 | (空) | -| WATCH_ROOM_ENABLED | 是否启用观影室功能(vercel部署不支持该功能,后续可能会开发外部服务器) | true/false | false | +| WATCH_ROOM_ENABLED | 是否启用观影室功能(vercel部署不支持该功能,可使用外部服务器) | true/false | false | +| WATCH_ROOM_SERVER_TYPE | 观影室服务器类型 | internal/external | internal | +| WATCH_ROOM_EXTERNAL_SERVER_URL | 外部观影室服务器地址(当 SERVER_TYPE 为 external 时必填) | WebSocket URL | (空) | +| WATCH_ROOM_EXTERNAL_SERVER_AUTH | 外部观影室服务器认证令牌(当 SERVER_TYPE 为 external 时必填) | 任意字符串 | (空) | | NEXT_PUBLIC_VOICE_CHAT_STRATEGY | 观影室语音聊天策略 | webrtc-fallback/server-only | webrtc-fallback | | NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD | 是否启用服务器离线下载功能(开启后也仅管理员和站长可用) | true/false | false | | OFFLINE_DOWNLOAD_DIR | 离线下载文件存储目录 | 任意有效路径 | /data | @@ -279,6 +283,24 @@ NEXT_PUBLIC_VOICE_CHAT_STRATEGY 选项解释: - webrtc-fallback:使用 WebRTC P2P 连接,失败时自动回退到服务器中转(推荐) - server-only:仅使用服务器中转(适用于无法建立 P2P 连接的网络环境) +### 外部观影室服务器部署 + +如果您在 Vercel 等无法运行 WebSocket 服务器的平台部署,或希望将观影室服务器独立部署,可以使用外部观影室服务器。 + +推荐使用由 [tgs9915](https://github.com/tgs9915) 开发的 [watch-room-server](https://github.com/tgs9915/watch-room-server) 项目进行部署。 + +**配置步骤:** + +1. 按照 [watch-room-server](https://github.com/tgs9915/watch-room-server) 的文档部署外部服务器 +2. 在 MoonTVPlus 中设置以下环境变量: + ```env + WATCH_ROOM_ENABLED=true + WATCH_ROOM_SERVER_TYPE=external + WATCH_ROOM_EXTERNAL_SERVER_URL=wss://your-watch-room-server.com + WATCH_ROOM_EXTERNAL_SERVER_AUTH=your_secure_token + ``` +3. 重启应用即可使用外部观影室服务器 + ## 弹幕后端部署 diff --git a/src/app/api/server-config/route.ts b/src/app/api/server-config/route.ts index 0be1393..1b54787 100644 --- a/src/app/api/server-config/route.ts +++ b/src/app/api/server-config/route.ts @@ -13,11 +13,12 @@ export async function GET(request: NextRequest) { const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; // 观影室配置从环境变量读取 + // 注意:不要暴露 externalServerAuth 到前端,这是敏感凭据 const watchRoomConfig = { enabled: process.env.WATCH_ROOM_ENABLED === 'true', serverType: (process.env.WATCH_ROOM_SERVER_TYPE as 'internal' | 'external') || 'internal', externalServerUrl: process.env.WATCH_ROOM_EXTERNAL_SERVER_URL, - externalServerAuth: process.env.WATCH_ROOM_EXTERNAL_SERVER_AUTH, + // externalServerAuth 不应该暴露给前端 }; // 如果使用 localStorage,返回默认配置 diff --git a/src/app/api/watch-room-auth/route.ts b/src/app/api/watch-room-auth/route.ts new file mode 100644 index 0000000..191fd56 --- /dev/null +++ b/src/app/api/watch-room-auth/route.ts @@ -0,0 +1,30 @@ +/* eslint-disable no-console */ + +import { NextRequest, NextResponse } from 'next/server'; + +import { getAuthInfoFromCookie } from '@/lib/auth'; + +export const runtime = 'nodejs'; + +/** + * GET /api/watch-room-auth + * + * 需要登录才能访问的接口,返回观影室外部服务器的认证信息 + * 这样可以避免将敏感的 externalServerAuth 暴露给未登录用户 + */ +export async function GET(request: NextRequest) { + console.log('watch-room-auth called: ', request.url); + + // 从 cookie 获取用户信息 + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 返回外部服务器认证信息 + const externalServerAuth = process.env.WATCH_ROOM_EXTERNAL_SERVER_AUTH; + + return NextResponse.json({ + externalServerAuth: externalServerAuth || null, + }); +} diff --git a/src/components/WatchRoomProvider.tsx b/src/components/WatchRoomProvider.tsx index 45c33c0..175c2ac 100644 --- a/src/components/WatchRoomProvider.tsx +++ b/src/components/WatchRoomProvider.tsx @@ -157,8 +157,27 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) { enabled: data.WatchRoom?.enabled ?? false, // 默认不启用 serverType: data.WatchRoom?.serverType ?? 'internal', externalServerUrl: data.WatchRoom?.externalServerUrl, - externalServerAuth: data.WatchRoom?.externalServerAuth, }; + + // 如果使用外部服务器,需要获取认证信息(需要登录) + if (watchRoomConfig.serverType === 'external' && watchRoomConfig.enabled) { + try { + const authResponse = await fetch('/api/watch-room-auth'); + if (authResponse.ok) { + const authData = await authResponse.json(); + watchRoomConfig.externalServerAuth = authData.externalServerAuth; + } else { + console.error('[WatchRoom] Failed to load auth info:', authResponse.status); + // 如果无法获取认证信息,禁用观影室 + watchRoomConfig.enabled = false; + } + } catch (error) { + console.error('[WatchRoom] Error loading auth info:', error); + // 如果无法获取认证信息,禁用观影室 + watchRoomConfig.enabled = false; + } + } + setConfig(watchRoomConfig); setIsEnabled(watchRoomConfig.enabled); diff --git a/src/types/watch-room.ts b/src/types/watch-room.ts index 8535670..6bbf1ca 100644 --- a/src/types/watch-room.ts +++ b/src/types/watch-room.ts @@ -129,7 +129,7 @@ export interface WatchRoomConfig { enabled: boolean; serverType: 'internal' | 'external'; externalServerUrl?: string; - externalServerAuth?: string; + externalServerAuth?: string; // 通过 /api/watch-room-auth 接口获取(需要登录) } // LocalStorage 存储的房间信息