增加主动清除弹幕缓存

This commit is contained in:
mtvpls
2025-12-10 00:13:08 +08:00
parent af12da4263
commit a5b02ea8d5
6 changed files with 34 additions and 346 deletions

View File

@@ -55,9 +55,10 @@ COPY --from=builder --chown=nextjs:nodejs /app/server.js ./server.js
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
run mv package.json package.json.bak
# 安装 Socket.IO 相关依赖standalone 模式不会自动包含)
RUN pnpm add socket.io@^4.8.1 socket.io-client@^4.8.1 --prod
run mv package.json.bak package.json
# 切换到非特权用户
USER nextjs

View File

@@ -1,307 +0,0 @@
# 观影室功能 - 实现文档
## 当前实现状态 ✅
### 已完成的核心功能
#### 1. 后端架构
- ✅ Socket.IO 服务器逻辑 (`src/lib/watch-room-server.ts`)
- 房间创建、加入、离开
- 成员管理
- 消息转发(播放状态、聊天消息)
- 心跳机制和自动清理
- ✅ 两种服务器模式
- **内部服务器**: 集成在 Next.js 应用中 (`server.js`)
- **外部服务器**: 独立运行的 Node.js 服务器 (`server/watch-room-standalone-server.js`)
#### 2. 前端功能
- ✅ 全局状态管理 (`WatchRoomProvider`)
- ✅ React Hook (`useWatchRoom`)
- ✅ 观影室首页 (`/watch-room`)
- 创建房间
- 加入房间
- 房间列表
- ✅ UI 组件
- 创建房间弹窗
- 加入房间弹窗
- 房间列表页面
- 聊天悬浮窗(全局)
- ✅ 导航集成
- 侧边栏添加观影室入口
- 底部导航栏添加观影室入口
#### 3. 同步功能
-**播放同步** (`usePlaySync` Hook)
- 房主播放/暂停/进度跳转实时同步
- 房主换集、换源自动同步
- 房员自动跟随房主操作
- 房员禁用控制器(显示"观影室模式"提示)
- 防抖机制避免频繁同步
- 进度差异超过2秒才同步避免网络抖动
-**直播同步** (`useLiveSync` Hook)
- 房主切换频道实时同步
- 房员自动跟随频道切换
- 延迟广播机制避免频繁触发
#### 4. 通用功能特性
- ✅ LocalStorage 自动重连
- ✅ 心跳机制每5秒
- ✅ 房主断开5分钟后自动删除房间
- ✅ 文字聊天
- ✅ 表情发送
- ✅ 响应式设计(移动端+电脑端)
---
## 使用方法
### 1. 启动开发服务器
```bash
# 使用内部 Socket.IO 服务器(推荐)
pnpm dev
# 或者使用外部 Socket.IO 服务器
pnpm watch-room:server # 在另一个终端运行
# 然后修改配置使用外部服务器
```
### 2. 访问观影室
1. 打开浏览器访问 `http://localhost:3000`
2. 点击侧边栏或底部导航的"观影室"按钮
3. 选择:
- **创建房间**: 输入房间信息和昵称
- **加入房间**: 输入房间号和昵称
- **房间列表**: 查看所有公开房间
### 3. 房间功能
创建或加入房间后:
- 右下角会出现聊天按钮
- 点击聊天按钮打开聊天窗口
- 可以发送文字和表情
---
## 待实现功能 🚧
### 高优先级(核心功能)
-**管理面板配置**
- 添加观影室开关
- 配置服务器类型(内部/外部)
- 外部服务器地址和鉴权配置
- API 端点: `/api/admin/watch-room`
### 中优先级(增强功能)
-**WebRTC 语音聊天**
- P2P 连接
- 服务器中转回退
- 麦克风和喇叭控制
-**房间成员列表**
- 显示在线成员
- 显示房主标识
-**权限控制优化**
- 房员禁用某些操作
- 房主踢人功能(可选)
### 低优先级(优化)
-**错误处理和重连优化**
-**性能优化**
-**更多表情支持**
---
## 技术架构
### 后端
- **Socket.IO 4.8.1**: WebSocket 通信
- **Next.js Custom Server**: 集成 Socket.IO
- **Node.js**: 独立服务器支持
### 前端
- **React 18**: UI 框架
- **TypeScript**: 类型安全
- **Socket.IO Client**: WebSocket 客户端
- **Tailwind CSS**: 样式框架
- **Lucide React**: 图标库
### 数据流
```
用户操作 → React Hook (useWatchRoom)
Socket.IO Client
Socket.IO Server (watch-room-server.ts)
房间成员 ← WebSocket 推送
```
---
## 文件结构
```
src/
├── types/
│ └── watch-room.ts # TypeScript 类型定义
├── lib/
│ ├── watch-room-server.ts # Socket.IO 服务器逻辑
│ └── watch-room-socket.ts # Socket 客户端管理
├── hooks/
│ ├── useWatchRoom.ts # 房间管理 Hook
│ ├── usePlaySync.ts # 播放同步 Hook
│ └── useLiveSync.ts # 直播同步 Hook
├── components/
│ ├── WatchRoomProvider.tsx # 全局状态管理
│ └── watch-room/
│ ├── CreateRoomModal.tsx # 创建房间弹窗
│ ├── JoinRoomModal.tsx # 加入房间弹窗
│ └── ChatFloatingWindow.tsx # 聊天悬浮窗
└── app/
└── watch-room/
├── page.tsx # 观影室首页
└── list/
└── page.tsx # 房间列表页面
server.js # Next.js 自定义服务器
server/
└── watch-room-standalone-server.js # 独立 Socket.IO 服务器
```
---
## 配置项
### 环境变量
```env
# Socket.IO 配置(可选)
WATCH_ROOM_ENABLED=true
WATCH_ROOM_SERVER_TYPE=internal # 或 external
WATCH_ROOM_EXTERNAL_URL=http://your-server:3001
WATCH_ROOM_EXTERNAL_AUTH=your_secret_key
```
### 运行时配置(将在管理面板中添加)
```typescript
interface WatchRoomConfig {
enabled: boolean; // 是否启用观影室
serverType: 'internal' | 'external'; // 服务器类型
externalServerUrl?: string; // 外部服务器地址
externalServerAuth?: string; // 外部服务器鉴权密钥
}
```
---
## 测试清单
### 基础功能测试
- [x] 创建房间
- [x] 加入房间(正确密码)
- [x] 加入房间(错误密码)
- [x] 查看房间列表
- [x] 发送文字消息
- [x] 发送表情
- [x] 心跳机制
- [x] 刷新页面自动重连
### 播放同步测试
- [x] 房主播放/暂停同步
- [x] 房主进度跳转同步
- [x] 房主换集同步
- [x] 房主换源同步
- [x] 房员禁用控制器
### 直播同步测试
- [x] 房主切换频道同步
- [x] 房员自动跟随频道
### 移动端测试
- [x] 底部导航显示正常
- [x] 聊天窗口适配移动端
- [x] 创建/加入弹窗适配移动端
- [x] 房间列表页面适配移动端
---
## 完整测试指南
### 测试场景 1: 创建房间并观看视频
1. **房主操作**:
- 访问观影室页面,点击"创建房间"
- 输入房间名称、描述和昵称
- 创建后,进入播放页面 (`/play`)
- 选择任意视频并播放
- 尝试播放/暂停、跳转进度、切换集数、切换源
2. **房员操作** (使用另一个浏览器或无痕模式):
- 访问观影室页面,点击"加入房间"
- 输入房间号和昵称
- 进入相同的播放页面
- 观察播放器自动同步房主操作
- 注意: 集数选择器上会显示"观影室模式"覆盖层,无法切换
### 测试场景 2: 聊天功能
1. 房主和房员都能看到右下角的绿色聊天按钮
2. 点击打开聊天窗口
3. 发送文字消息和表情
4. 验证双方都能收到消息
5. 测试最小化和关闭功能
### 测试场景 3: 直播同步
1. **房主操作**:
- 在房间内,进入直播页面 (`/live`)
- 切换不同频道
2. **房员操作**:
- 同样进入直播页面
- 观察频道自动跟随房主切换
### 测试场景 4: 自动重连
1. 房主或房员刷新页面
2. 验证自动重连到原房间
3. 验证聊天记录不丢失
4. 验证播放状态继续同步
---
## 下一步计划
1. **添加管理面板配置**
-`src/app/admin/page.tsx` 添加观影室配置项
- 保存配置到数据库
2. **实现 WebRTC 语音聊天** (可选)
- 创建 WebRTC 连接管理
- 添加麦克风/喇叭控制按钮
- 实现服务器中转回退
---
## 已知问题
1. **语音聊天未实现**: 仅支持文字和表情
2. **房间成员列表未显示**: 可以在聊天窗口顶部看到在线人数,但没有详细成员列表
---
## 贡献
欢迎提交 Issue 和 Pull Request
---
最后更新: 2025-12-06

View File

@@ -15,6 +15,7 @@ import ChatFloatingWindow from '../components/watch-room/ChatFloatingWindow';
import { DownloadProvider } from '../contexts/DownloadContext';
import { DownloadBubble } from '../components/DownloadBubble';
import { DownloadPanel } from '../components/DownloadPanel';
import { DanmakuCacheCleanup } from '../components/DanmakuCacheCleanup';
const inter = Inter({ subsets: ['latin'] });
export const dynamic = 'force-dynamic';
@@ -128,6 +129,7 @@ export default async function RootLayout({
<SiteProvider siteName={siteName} announcement={announcement}>
<WatchRoomProvider>
<DownloadProvider>
<DanmakuCacheCleanup />
{children}
<GlobalErrorIndicator />
<ChatFloatingWindow />

View File

@@ -161,6 +161,15 @@ function PlayPageClient() {
const { version } = await versionResponse.json();
// 如果版本号为 0说明去广告未设置清空缓存并跳过
if (version === 0) {
console.log('去广告代码未设置(版本 0清空缓存');
localStorage.removeItem('custom_ad_filter_code_cache');
localStorage.removeItem('custom_ad_filter_version_cache');
customAdFilterCodeRef.current = '';
return;
}
// 如果版本号不一致或没有缓存,才获取完整代码
if (!cachedVersion || parseInt(cachedVersion) !== version) {
console.log('检测到去广告代码更新(版本 ' + version + '),获取最新代码');
@@ -3642,43 +3651,6 @@ function PlayPageClient() {
</span>
</button>
{/* IDM */}
<button
onClick={(e) => {
e.preventDefault();
// 获取正确的代理 URL
const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : '';
const origin = `${window.location.protocol}//${window.location.host}`;
const proxyUrl = externalPlayerAdBlock
? `${origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}${tokenParam}`
: videoUrl;
// 唤起 IDM 下载器
window.open(`idm://${encodeURIComponent(proxyUrl)}`, '_blank');
}}
className='group relative flex items-center justify-center gap-1 w-8 h-8 lg:w-auto lg:h-auto lg:px-2 lg:py-1.5 bg-white hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 text-xs font-medium rounded-md transition-all duration-200 shadow-sm hover:shadow-md cursor-pointer overflow-hidden border border-gray-300 dark:border-gray-600 flex-shrink-0'
title='IDM'
>
<svg
className='w-4 h-4 flex-shrink-0'
viewBox='0 0 48 48'
xmlns='http://www.w3.org/2000/svg'
>
<path fill='#1976d2' d='M20,13c-8.837,0-16,6.044-16,13.5S11.163,40,20,40s16-6.044,16-13.5S28.837,13,20,13z M20,30 c-4.418,0-8-2.91-8-6.5s3.582-6.5,8-6.5s8,2.91,8,6.5S24.418,30,20,30z'/>
<path fill='#4caf50' d='M20,13c-6.879,0-12.726,3.669-14.987,8.809C5.011,21.874,5,21.936,5,22c0,7.18,4,14,17,15 c5.472,0.421,10.355-3.997,13.463-7.083C35.801,28.823,36,27.683,36,26.5C36,19.044,28.837,13,20,13z M20,30c-4.418,0-8-2.91-8-6.5 s3.582-6.5,8-6.5s8,2.91,8,6.5S24.418,30,20,30z'/>
<path fill='#ffeb3b' d='M31,33l-1.382-17.27C26.939,14.024,23.615,13,20,13c-5.319,0-10.014,2.2-12.918,5.572 C6.461,21.613,6.806,25.806,11,30C19,38,31,33,31,33z M20,17c4.418,0,8,2.91,8,6.5S24.418,30,20,30s-8-2.91-8-6.5S15.582,17,20,17z'/>
<path fill='#ff1744' d='M24.563,28.835C23.268,29.568,21.697,30,20,30c-4.418,0-8-2.91-8-6.5s3.582-6.5,8-6.5 c0.043,0,0.084,0.005,0.127,0.005L19,14c0,0-12,2-9,10s15,6,15,6L24.563,28.835z'/>
<circle cx='30' cy='20' r='14' fill='#29b6f6'/>
<path fill='#4caf50' d='M42,24c0,0,0,2-5,1s-6,2-6,2l-1.986,6.95C29.341,33.973,29.667,34,30,34 c7.459,0,13.538-5.838,13.959-13.192C42.832,21.508,42,24,42,24z'/>
<path fill='#4caf50' d='M32.208,10.347C31.719,10.487,31.302,10.698,31,11c-2,2-4,0-4,0s1.373-1.648,3.256-3.072 c0.959-0.726,0.043-2.255-1.026-1.702C28.823,6.436,28.411,6.691,28,7c-1.684,1.263-2.301,0.749-2.455-0.264 C20,8.598,16,13.827,16,20c0,0.425,0.04,0.839,0.077,1.254C17.042,21.932,18.81,22.532,22,22c6-1,4,7,4,7 s-0.196,0.458-0.503,1.135c0.612-0.375,1.239-0.832,1.884-1.4C28.332,28.507,29,27.469,29,25c0-1.716-0.52-3.043-1.127-4.008 c-0.839-1.333,0.716-2.908,2.037-2.051C29.94,18.96,29.97,18.98,30,19c3,2,1-2,1-2s-1.531-3.061,1.968-4.811 C34.048,11.65,33.368,10.014,32.208,10.347z'/>
<polygon fill='#8bc34a' points='20,23 24,44 44,29'/>
<polygon fill='#4caf50' points='24,44 29,34 20,23'/>
<polygon fill='#33691e' points='24,44 29,34 44,29'/>
</svg>
<span className='hidden lg:inline max-w-0 group-hover:max-w-[100px] overflow-hidden whitespace-nowrap transition-all duration-200 ease-in-out text-gray-700 dark:text-gray-200'>
IDM
</span>
</button>
{/* PotPlayer */}
<button
onClick={(e) => {

View File

@@ -0,0 +1,18 @@
'use client';
import { useEffect } from 'react';
import { initDanmakuModule } from '@/lib/danmaku/api';
/**
* 弹幕缓存清理组件
* 在应用启动时执行一次过期缓存清理
*/
export function DanmakuCacheCleanup() {
useEffect(() => {
// 只在客户端执行一次
initDanmakuModule();
}, []);
// 这个组件不渲染任何内容
return null;
}

View File

@@ -227,7 +227,9 @@ export async function clearExpiredDanmakuCache(): Promise<number> {
cursor.continue();
} else {
if (deletedCount > 0) {
console.log(`已清除 ${deletedCount} 个过期弹幕缓存`);
}
resolve(deletedCount);
}
};