# 会议录制实现进度（Phase B1→B4）

启动日期：2026-05-18
负责人：Cascade pair-programming session

## 全局架构

```
[host 工具栏] → POST /meeting/rooms/{code}/recording/toggle
   ↓
[Go MeetingRecordingService]
   ├─ 校验 host 权限 + 会议状态
   ├─ 落库 meeting_recordings(status=recording)
   └─ 调 HTTPMediaOrchestrator.StartRecording(roomCode, producerIDs)
       ↓
[media-server /internal/v1/recordings]
   ├─ 为每个 Producer 建 PlainTransport（rtcpMux:false, comedia:false）
   ├─ consume 该 Producer 拿到 negotiated rtpParameters
   ├─ 选未占用 UDP 端口对（rtp+rtcp）给 ffmpeg
   ├─ 写 SDP 文件（含 audio/video pt、codec、rtpmap、fmtp）
   ├─ spawn ffmpeg: -protocol_whitelist file,udp,rtp -i input.sdp -c copy output.mp4
   └─ transport.connect({ ip, port: ffmpegPort, rtcpPort: ffmpegRtcpPort })
       ↓
[ffmpeg 写盘 → /tmp/recordings/{recordingId}.mp4]
       ↓
[host 再次 toggle → 停止]
   ├─ media-server SIGINT ffmpeg → wait exit → close transports/consumers
   ├─ POST /api/v1/meeting/_internal/recordings/{id}/finalize（callback）
   └─ Go 流式拉取 mp4 → MinIO → 落库 status=ready + file_url + size + duration
       ↓
[WS 广播 meeting.recording.started/stopped 给所有成员]
```

## 阶段拆分

### Phase B1 — 媒体层录制管线 + Go 控制端 ✏️ 进行中
**media-server**：
- [ ] `services/recording.service.ts`：起停录制、SDP 生成、ffmpeg 进程管理、端口池
- [ ] `routes/recording.route.ts`：`POST /internal/v1/recordings`、`DELETE /internal/v1/recordings/:id`、`GET /internal/v1/recordings/:id`
- [ ] `schemas/recording.schema.ts`：zod 校验
- [ ] `app.ts`：注册路由
- [ ] 端口池实现 `utils/port-pool.ts`

**Go 后端**：
- [ ] `app/meeting/model/meeting_recording.go`：表 ORM
- [ ] `app/meeting/dao/meeting_recording_dao.go`
- [ ] `app/meeting/service/interfaces.go` 扩展 MediaOrchestrator: `StartRecording / StopRecording`
- [ ] `app/meeting/service/http_media_orchestrator.go` 实现 RPC
- [ ] `cmd/server/main.go` AutoMigrate 加入 MeetingRecording

### Phase B2 — 业务层 + 持久化
- [ ] `service/meeting_recording_service.go`：StartRecording/StopRecording/ListRecordings/GetRecording
- [ ] `controller/meeting_controller.go` 新增 4 个端点
- [ ] `router.go` 挂载路由
- [ ] `provider.go` wire 注册
- [ ] 内部回调端点 `/_internal/recordings/{id}/finalize`：media-server 录完后回调，Go 拉文件→ MinIO → 更新 DB
- [ ] MinIO 流式上传

### Phase B3 — WS 广播 + 自动停录
- [ ] `constants/meeting.go`：MeetingWSEventRecordingStarted/Stopped
- [ ] `meeting_signal_service.go` / `meeting_broadcaster.go` 广播
- [ ] EndRoom / HandleEmptyRoomExpired 自动 StopRecording
- [ ] 前端 `constants/meeting.js` + store 监听

### Phase B4 — 前端 UI
- [ ] `MeetingToolbar.vue`：录制按钮（仅 host 可见）+ 红点动画
- [ ] `room.vue`：顶部"REC"指示条（任意成员都可见）
- [ ] 新页面 `pages/meeting/recordings.vue`：列表 + 下载

## 已知风险与决策

1. **ffmpeg 必须可用**：媒体服务的运行环境（开发：本地 PATH；生产：Docker 镜像 apt install ffmpeg）。Dockerfile 暂不改，由你部署时确认。
2. **PlainTransport 端口分配**：用 dgram 试探可用 UDP 端口；初始范围 50000-59999，避开 mediasoup `40000-40199`。
3. **ffmpeg 启停时序**：录制启动顺序 = 起 ffmpeg(listen) → 100ms 延迟 → mediasoup transport.connect → consume；停止顺序 = SIGINT ffmpeg → wait exit → close transports/consumers。
4. **录制视频编码**：用 `-c:v copy -c:a copy` 直接复制 RTP 载荷为 mp4（不重编码），节省 CPU。注意：浏览器端 VP8/VP9 muxed 进 mp4 可能不被普通播放器识别，必要时改 `-c:v libx264 -preset ultrafast`。本期先用 copy。
5. **DB 表 schema**：参考下方 §SQL。
6. **Phase B 整体不强制阻塞**：每完成一个阶段都可以暂停验证。

## SQL

```sql
CREATE TABLE meeting_recordings (
  id              BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  room_id         BIGINT UNSIGNED NOT NULL,
  room_code       VARCHAR(32)     NOT NULL,
  started_by      BIGINT UNSIGNED NOT NULL,
  status          VARCHAR(16)     NOT NULL DEFAULT 'recording',  -- recording/uploading/ready/failed
  remote_id       VARCHAR(64),                                   -- media-server 侧的 recordingId
  file_url        VARCHAR(512),
  file_object     VARCHAR(255),
  size_bytes      BIGINT UNSIGNED DEFAULT 0,
  duration_sec    INT UNSIGNED    DEFAULT 0,
  failure_reason  VARCHAR(255),
  started_at      DATETIME        NOT NULL,
  stopped_at      DATETIME,
  created_at      DATETIME        NOT NULL,
  updated_at      DATETIME        NOT NULL,
  KEY idx_room (room_id),
  KEY idx_room_code (room_code),
  KEY idx_started_by (started_by)
);
```

## 当前会话进度

- ✅ 调研完成：MinIO 已就绪、media-server 架构吃透、HTTPMediaOrchestrator 模式吃透
- ⏩ 下一步：写 media-server 端口池 + recording.service.ts
