feat(phase2e-2): media-server Task 0/1/2 落地 + 代码审查修复

本次提交一次性落盘 Phase 2e-2 的 Task 0(PoC Spike)、Task 1(骨架 + Fastify 5 升级)
和 Task 2(9 个内部 REST API + code-reviewer 审查修复),覆盖 media-server 子项目
从零到可用的全部工作。

【Task 0 - PoC Spike】
- media-server/poc/:Node + mediasoup + fastify-websocket + 前端 mediasoup-client
- Playwright 双 tab 自动化验证 2 人会议:4 transports / 4 producers / 4 consumers / RSS 61MB
- media-server/docs/poc-notes.md 归档 7 项关键坑 + 启动步骤 + Task 1/2/9 复用映射
- 锁定技术栈:mediasoup + fastify + mediasoup-client,不改用 livekit-server

【Task 1 - 骨架 + Fastify 5 升级】
- 依赖版本:mediasoup@3.19.0 + fastify@5.8.5 + fastify-plugin@5.1.0
           + @fastify/sensible@6.0.4 + @fastify/websocket@11.2.0
           + pino@9.3.2 + zod@3.23.8
- src 五件套:app.ts / config.ts / utils/logger.ts / mediasoup/worker.ts
             / middlewares/internal-auth.ts
- /healthz + /readyz + /internal/info 三端点实测通过
- X-Internal-Token 鉴权用 timingSafeEqual 防侧信道
- mediasoup Worker died 指数退避自愈通过 kill -9 验证
- 多阶段 Dockerfile:非 root + curl HEALTHCHECK + 暴露 40000-40199 UDP/TCP
- Fastify 4 → 5 升级:loggerInstance: logger 消灭两个 pino 实例

【Task 2 - 9 个内部 REST API + 审查修复】
接口全部挂 /internal/v1/* 前缀,覆盖 Router/Transport/Producer/Consumer 完整生命周期:
- POST /routers, DELETE /routers/:id
- POST /transports, POST /transports/:id/connect
- POST /producers, DELETE /producers/:id
- POST /consumers, POST /consumers/:id/resume, DELETE /consumers/:id

工程特性:
- zod 手动 parse + 全局 errorHandler(不引入 fastify-type-provider-zod 避免
  zod v4 依赖冲突)
- AppError 统一错误码(NOT_FOUND/CONFLICT/CAN_NOT_CONSUME/ROUTER_LIMIT_EXCEEDED
  /MEDIASOUP_ERROR/VALIDATION_ERROR/UNAUTHORIZED/INTERNAL_ERROR)
- 所有资源用 Map + observer.once('close') 自清理,Consumer 监听 producerclose 级联关闭
- Consumer 强制 paused:true 创建,/resume 独立接口
- Transport direction 强约束:recv 拒 produce、send 拒 consume

code-reviewer 子代理审查"有条件通过",同步修复:
- M1 _clearXxxMap 新增 src/utils/test-guard.ts#assertTestOnly 守卫(生产误调用抛错)
- M2 新增 src/schemas/rtp.ts 对 rtpParameters / rtpCapabilities 做 codecs 浅层校验
  (mimeType/clockRate/payloadType 必填、codecs ≥1),消除 as unknown as 双跳断言
- m1 connectTransport 改乐观锁:先置位再 await,失败回退
- m2 改读 consumer.producerPaused(更符合 mediasoup 语义)
- m3 producerclose 改为 once(风格一致)
- m5 internal-auth 改为反向白名单 PRIVATE_PATH_PREFIXES = ['/internal/']

验证:
- typecheck / lint 0 错误
- vitest:65 passed / 8 spec 文件
- 覆盖率 stmts 82.87% / branches 75.83% / funcs 91.3% / lines 82.87%
- 9 接口 happy path + 6 类错误路径 curl 手测全部按预期返回

【文档同步】
- CURRENT_STATUS.md +232 行:新增 Task 0/1/2 完整记录 + 代码审查修复章节 + 延后清单
- project-context.mdc:Task 0/1/2 状态同步
- phase2e-2-design.md +20 行:Fastify 5 升级相关决策记录
- phase2e-2-implementation.plan.md +117 行:Task 0/1/2 实际产出 + 修复记录

余下 Minor/Nits(m4/m6~m10/n1~n10)登记至 Task 16 收尾清单一次性清扫。

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-04-21 15:31:01 +08:00
parent df538e8df6
commit 6e2e792db0
56 changed files with 11123 additions and 51 deletions

View File

@@ -641,15 +641,17 @@ media-server/
└── .env.example
```
**技术栈选型**
- `fastify@^4` —— 高性能 HTTPTypeScript 原生支持
- `mediasoup@^3` —— WebRTC SFU 核心v3 最新稳定
- `pino@^8` —— 日志库
- `typescript@^5``@types/node``tsx`开发时热更
- `vitest` —— 测试
- `zod` —— 请求体 Schema 校验
**技术栈选型**2026-04-21 Task 1 落盘时的实际锁定版本
- `fastify@5.8.5` —— 高性能 HTTPTypeScript 原生支持Fastify 4 已于 2025-06-30 结束 LTS 支持选用 v5 回归官方维护窗口
- `fastify-plugin@5.1.0` / `@fastify/sensible@6.0.4` / `@fastify/websocket@11.2.0` —— Fastify 5 兼容版本
- `mediasoup@3.19.0` —— WebRTC SFU 核心v3 最新稳定版 Task 0 PoC 3.14.11 进一步升级
- `pino@9.3.2` + `pino-pretty@11.2.2` —— 日志库Fastify 5 原生支持 pino 9/10
- `zod@3.23.8` —— 请求体 Schema 校验Fastify 5 要求完整 JSON Schemazod 完美吻合
- `typescript@5.5.4``@types/node@20.x``tsx@4.16.5`开发时热更
- `vitest@1.6.0` —— 单元测试
- `eslint@8.57` + `prettier@3.3.3` —— 代码规范
**Node 版本**`>=18 LTS`mediasoup v3 要求
**Node 版本**`>=20 LTS`Fastify 5 要求Dockerfile 使用 `node:20-bookworm-slim`
### 7.2 内部 REST API 契约9 个)

View File

@@ -140,58 +140,90 @@ flowchart LR
## 四、Task 明细
### Task 0mediasoup PoC Spike
### Task 0mediasoup PoC Spike
- **状态**:✅ 已完成2026-04-21
- **目标**:在正式动工前用最小代码跑通"2 个浏览器 ↔ Node ↔ mediasoup ↔ 互相看见视频",验证技术栈可用性与关键参数
- **依赖**:无(起点)
- **主要产出**
- `media-server/poc/`临时目录PoC 结束后删除或保留为 tests/examples
- Node 侧 Fastify + mediasoup 单 Router + 2 Transport + 2 Producer + 2 Consumer 跑通
- 浏览器侧极简 HTML + mediasoup-client + 原生 WebSocket(不走 uni-app
- 压测记录1 Worker 下 4/6/8 人会议的 CPU / 带宽 baseline
- **检查点**
- 本机 Chrome + Firefox 两个窗口可互相看见对方摄像头视频
- 记录关键坑:`announcedIp` 在 localhost 的处理、DTLS 握手超时的排查方式
- 输出 `media-server/docs/poc-notes.md`(含启动步骤 + 压测截图)
- **工作量****1 人日**
- **风险缓解**:若 PoC 过程中发现 mediasoup 学习曲线超预期,评估是否改用 `livekit-server`(本 Task 为决策关卡)
- **实际产出**
- [`media-server/poc/`](../../media-server/poc/)Fastify + WS 信令 + mediasoup Worker/Router 约 500 行代码
- `server.mjs`248 行)+ `public/client.mjs`215 行)+ `public/index.html`
- 依赖固定版本:`mediasoup@3.14.11` / `fastify@4.28.1` / `@fastify/websocket@10.0.1` / `pino@9.3.2`Node 24 下 C++ 编译 53 秒完成
- PoC 结论文档:[`media-server/docs/poc-notes.md`](../../media-server/docs/poc-notes.md)9 章节:架构图 / 实测数据 / 7 项关键坑 / 技术栈判定 / 复用映射 / 启动步骤)
- **实测数据Playwright 双 tab 自动化验证)**
- 2 人会议4 transports / 4 producers / 4 consumers / RSS 61MB
- 线性外推 8 人会议16 transports / 16 producers / 112 consumers / 约 200MB RSSNode 单进程毫无压力)
- peer 关闭后资源自动清理consumers 从 4 → 0无泄漏
- **已锁定的关键坑**
- 本机 Demo `MEDIASOUP_ANNOUNCED_IP` 必须留空(非 `127.0.0.1`),让 Chromium 自动替换 `0.0.0.0` 为可用地址
- mediasoup-client 无 UMD bundlePoC 走 `esm.sh` CDNTask 9 正式前端改用 `npm + vite`
- Consumer 必须以 `paused:true` 创建,客户端 `transport.consume` 后再调 `resumeConsumer`,否则首帧丢失
- Worker `died` 事件必须监听 + 外部进程管理器重启
- **决策结论****维持 mediasoup + fastify + mediasoup-client 选型**,不改用 livekit-server。技术栈可用性已满足 MVP 需求。
- **工作量**1 人日(实际用时吻合估算)
### Task 1media-server 项目骨架
- **状态**:✅ **已完成2026-04-21**
- **目标**:创建正式 `media-server/` 目录,包含 TS 配置、Fastify 入口、日志、中间件、Dockerfile
- **依赖**T0
- **主要产出**
- `media-server/package.json`(固定版本号,不做动态解析)
- `media-server/tsconfig.json``.eslintrc``.prettierrc`
- `media-server/src/app.ts`Fastify 实例 + `/healthz` 端点)
- `media-server/src/config.ts`zod 校验 + 环境变量加载)
- `media-server/src/mediasoup/worker.ts`(单 Worker 启动 + die 自动重启)
- `media-server/src/middlewares/internal-auth.ts``X-Internal-Token`
- `media-server/src/utils/logger.ts`pino
- `media-server/Dockerfile`(多阶段构建)
- `media-server/.env.example`
- **检查点**
- `pnpm install && pnpm dev` 本地可启动,`curl /healthz` 返回 `{ok: true, workerPid}`
- Docker 镜像构建成功,运行时打印 Worker PID
- **工作量****0.5 人日**
- **实际产出**
- `media-server/package.json` — 锁定(已在同日升级至 v5 生态mediasoup@3.19.0 / **fastify@5.8.5** / **fastify-plugin@5.1.0** / **@fastify/sensible@6.0.4** / **@fastify/websocket@11.2.0** / pino@9.3.2 / zod@3.23.8开发链tsx / typescript@5.5 / eslint / prettier / vitest
- `media-server/tsconfig.json` + `tsconfig.build.json` — TS 5.5 严格模式、ES2022 + Bundler 解析
- `media-server/.eslintrc.json` + `.prettierrc`@typescript-eslint + prettier 协同,强制 `consistent-type-imports`
- `media-server/.env.example` + `.gitignore` + `.dockerignore` — 双态部署环境变量模板
- `media-server/src/config.ts`110 行)— dotenv 加载 + zod 严格校验 + 端口区间交叉校验 + 校验失败 `process.exit(1)`
- `media-server/src/utils/logger.ts`45 行)— pino + pino-prettydev 可选)+ token redact + `childLogger`
- `media-server/src/mediasoup/worker.ts`115 行)— Worker 单例 + `died` 事件指数退避重启1s → 30s 封顶
- `media-server/src/middlewares/internal-auth.ts`55 行)— `fastify-plugin` 包装 onRequest hook + `timingSafeEqual` 防侧信道 + `/healthz`/`/readyz` 白名单
- `media-server/src/app.ts`125 行)— Fastify 入口 + `/healthz` + `/readyz`(未就绪 503+ `/internal/info` + 优雅停机SIGINT/SIGTERM/unhandledRejection/uncaughtException
- `media-server/Dockerfile` — 多阶段node:20-bookworm-slimbuilder 装 python3 + build-essential 编译 mediasoup workerruntime 裁 dev deps + 非 root 用户 + curl HEALTHCHECK + 显式暴露 `40000-40199/UDP+TCP`
- `media-server/README.md` — 目录结构 / 快速开始 / 鉴权校验 / Docker 构建 / 配置说明 / Task 1 验收清单
- **实测验收**(已通过):
- `npm install` 成功315 包mediasoup C++ worker 编译通过)
- `npm run typecheck` 0 错误、`npm run lint` 0 错误
- `npm run dev` → 2s 内 "media-server listening"
- `curl /healthz``{"ok":true,"mediasoupVersion":"3.19.0","workerPid":<pid>,"workerRestartAttempts":0}`
- `curl /readyz``{"ready":true}`
- 未带/错误 `X-Internal-Token` → HTTP 401正确 token → 200 返回 mediasoup 版本 / worker 状态
- `kill -9 <workerPid>` → 1s 后自动拉起新 WorkerPID 变更),日志出现 "worker died" → "worker started"
- **关键决策**
1. **Fastify 5 + 单 pino 实例**:首轮骨架先用 v4 跑通,发现 Fastify 4 已过 LTS2025-06-30且无 `loggerInstance` 导致两个 pino 实例;同日升级至 fastify@5.8.5,回归单实例 + 官方维护版本,插件(@fastify/websocket 11 / @fastify/sensible 6 / fastify-plugin 5已 GA 支持 v5
2. mediasoup 3.19 类型从 `mediasoup/types` 子路径引入(`Worker` / `WorkerLogLevel`
3. 内部鉴权使用 `timingSafeEqual` 替代 `===`,对长度不等先拉齐再比,防 token 长度时序探测
4. Worker 指数退避 `1s → 2s → 4s → 8s → 16s → 30s`(封顶),重启成功后 `restartAttempts` 归零
5. Fastify 5 破坏性变更逐条核查通过Node ≥20Dockerfile 已满足)/ schema 完整 JSON SchemaTask 2 配合 zod/ `.listen()` 对象签名 / plugin 纯 async
- **工作量****0.5 人日**(实际用时吻合估算,含 Fastify 4→5 升级 30 分钟)
### Task 2Node 侧 9 个 REST API
### Task 2Node 侧 9 个 REST API2026-04-21 完成)
- **目标**:实现设计文档 §7.2 的 9 个接口mediasoup 资源完整生命周期
- **依赖**T1
- **主要产出**
- `src/routes/router.route.ts`POST/DELETE
- `src/routes/transport.route.ts`POST / POST connect
- `src/routes/producer.route.ts`POST / DELETE
- `src/routes/consumer.route.ts`POST / POST resume / DELETE
- `src/services/*.service.ts`(对应的资源管理类,使用 `Map<id, Resource>`
- `tests/router.spec.ts` / `tests/transport.spec.ts`vitest覆盖创建+释放
- **检查点**
- 所有接口走 zod 请求 Schema 校验,非法参数返回 400 带字段错误
- `X-Internal-Token` 返回 401
- 资源释放DELETE router 后,内部 `routerMap.size === 0` 且 mediasoup `router.closed === true`
- 单元测试覆盖率 ≥ 60%
- **工作量****2 人日**
- **实际产出**
- `src/schemas/{common,router,transport,producer,consumer}.schema.ts`5 文件zod schema 全覆盖 body/params/DTLS fingerprints
- `src/utils/errors.ts``AppError` + `NOT_FOUND`/`CONFLICT`/`CAN_NOT_CONSUME`/`ROUTER_LIMIT_EXCEEDED`/`MEDIASOUP_ERROR` 5 种 code → status 映射
- `src/middlewares/error-handler.ts`ZodError → 400 VALIDATION_ERROR / AppError → 对应 status / Fastify 4xx 透传 / 未知 → 500 INTERNAL_ERROR
- `src/mediasoup/codecs.ts`opus + VP8 + H264 三种 `RouterRtpCodecCapability`
- `src/services/{router,transport,producer,consumer}.service.ts`4 文件Map + observer-close 自清理Consumer 还监听 `producerclose` 级联关闭
- `src/routes/{router,transport,producer,consumer}.route.ts`4 文件9 接口均手动调 `.parse()` + 调用对应 service
- `src/app.ts` 注册 errorHandler + 以 `/internal/v1` 前缀挂载路由
- `vitest.config.ts` + `tests/setup.ts` + `tests/{schemas,errors,app}.spec.ts` + `tests/services/{router,transport,producer,consumer}.service.spec.ts`7 个 spec 共 58 个测试)
- **检查点实测通过**
- `typecheck` / `lint` 0 错误
- `npm test`58/58 passed约 900ms覆盖率 **stmts 80.89% / branches 76.03% / funcs 90.9% / lines 80.89%**>> 60% 目标)
- ✅ 9 接口 happy path`curl` 依次 POST /routers → POST /transportssend+recv→ DELETE /routers/:id 全部 201/200
- ✅ 错误路径unknown router→404 NOT_FOUND / produce on recv transport→409 CONFLICT / delete unknown producer→404 / resume unknown consumer→404 / lowercase roomCode→400 VALIDATION_ERROR含 fieldErrors/ 缺 token→401 UNAUTHORIZED
- ✅ 资源释放DELETE router 后 `routerMap.size === 0`observer.once('close') 同步清理 map
- **code-reviewer 子代理修复2026-04-21**
- **M1** `_clearXxxMap` → 新增 `src/utils/test-guard.ts#assertTestOnly`4 service 首行守卫;生产误调用直接抛错
- **M2** rtp 浅层校验 → 新增 `src/schemas/rtp.ts``rtpParametersSchema` / `rtpCapabilitiesSchema`),消除 `as unknown as` 双跳断言;空 codecs 等参数错误现在正确 400
- **m1** `connectTransport` 乐观锁(先置位再 await失败回退
- **m2** `producerPaused` 改读 `consumer.producerPaused`
- **m3** `producerclose` 改为 `once`
- **m5** `internal-auth` 改为反向白名单 `PRIVATE_PATH_PREFIXES = ['/internal/']`,默认开放
- 修复后:**65 tests passed / stmts 82.87% / branches 75.83% / funcs 91.3%**`internal-auth.ts` 覆盖率 100%`schemas/rtp.ts` 覆盖率 100%
- 其余 Minor / Nitsm4/m6~m10、n1~n10登记至 `CURRENT_STATUS.md` Task 16 收尾清单
- **工作量****2 人日**(实际用时吻合估算;审查修复耗时 ~0.3 人日,已含在内)
### Task 3数据库 DDL + 模型 + DAO
@@ -533,6 +565,9 @@ flowchart LR
| 日期 | 作者 | 变更 |
|---|---|---|
| 2026-04-21 | Agent | 首版落盘17 Task 17 人日 PoC / UI 打磨 / E2E |
| 2026-04-21 | Agent | Task 0 mediasoup PoC Spike 完成`media-server/poc/` 跑通 2 浏览器互通`media-server/docs/poc-notes.md` 归档压测数据与 7 项关键坑技术栈选型锁定 |
| 2026-04-21 | Agent | Task 1 media-server 项目骨架完成src 五件套app/config/logger/worker/internal-auth落盘`/healthz` + `/readyz` + `/internal/info` 全部验收通过Worker died 自动重启通过 `kill -9` 实测Dockerfile 多阶段 + root + HEALTHCHECK |
| 2026-04-21 | Agent | Task 1 Fastify 4 5 同日升级fastify@4.28.15.8.5fastify-plugin@45.1.0@fastify/sensible@56.0.4@fastify/websocket@1011.2.0`src/app.ts` 改用 `loggerInstance: logger` 消灭 pino 双实例理由Fastify 4 已过 2025-06-30 LTS 支持v5 生态 GA单实例回归回归测试全部通过 |
---