14 · 媒体与实时语音
Buzz 把持久媒体和实时语音分成两条完全不同的链:媒体经 Blossom/NIP-98 鉴权、严格内容验证后进入 S3-compatible storage;Huddle 音频经 WebSocket/二进制帧进入 room/mesh,强调低延迟、背压和租约围栏。两者只在客户端体验上相邻,可靠性模型不同。
1. 媒体上传链
client file
│ Blossom auth / NIP-98
▼
Relay HTTP handler
├─ size/MIME/magic validation
├─ format-specific structural checks
├─ metadata/privacy rejection
├─ optional thumbnail + blurhash
└─ S3 put
├─ immutable/hashed blob key
└─ community-scoped metadata sidecarbuzz-media 是验证与存储库,Axum handler 留在 Relay。这保持 library 不依赖具体 HTTP 框架,并让 tenant/auth 信息由 Relay 注入。
2. 为什么不能相信 MIME
客户端声明 image/jpeg 只是提示。验证器交叉检查:
- 文件 magic bytes 与声明 MIME。
- JPEG marker、PNG chunks、WebP RIFF、GIF 结构。
- 图像尺寸/像素预算,防解压炸弹。
- MP4 box 长度、嵌套深度、codec 与
moov/mdat次序。 - 通用文件扩展与危险类型限制。
格式 parser 的目标不是完整解码,而是以小成本拒绝明显伪造、溢出与资源攻击。
3. 隐私元数据
图片可能含 EXIF/GPS/XMP,泄露位置、设备和编辑历史。Buzz 选择在服务端验证/拒绝敏感 metadata,而不完全依赖客户端“上传前清理”。
物理 blob key 与社区 sidecar metadata 分开:同 hash blob 可按存储策略复用,但 tenant/uploader/network attribution 必须在社区语境内查询,不能靠对象名暴露。
4. 派生资源
图片可生成 thumbnail/blurhash;这些是原 blob 的派生物:
- 原 blob 成功持久化是主事实。
- 派生失败可重试,不应伪造原上传成功。
- 派生 key 必须绑定原 hash/variant,避免旧缩略图覆盖新内容。
- 输出再次受尺寸与编码预算限制。
5. Huddle 组件
实时语音位于 Relay audio/ 与 Desktop huddle/:
Desktop capture / preprocessing
│ encoded audio frames
▼
Relay huddle handler
├─ join/auth/channel membership
├─ room manager (community, channel)
├─ control queue (reliable/bounded)
├─ audio queue (drop on full)
└─ optional mesh owner/tunnel
│
▼
Desktop jitter/playout + optional STT/TTS6. 协议版本协商
Room 的第一个 peer 固定 wire protocol version。后续 peer 若不兼容被拒绝。若 room 已满与版本不匹配同时成立,优先返回 full,避免外部探测者通过错误类型推断房间内部版本/成员状态。
soft cap 约 25 peers,peer index 用 u8。具体 cap 同时受带宽、编码和 UI 设计限制,不应仅因为 index 可表示 255 就放宽。
7. 控制与音频背压分离
控制帧(join/leave/mute/renew)影响状态,需要有界可靠处理;音频帧追求低延迟,队列满时丢旧/新帧比积压数秒更合理。
control full → slow/close, preserve state correctness
audio full → drop frame, preserve real-time latency如果共用一个队列,音频洪峰会让 leave/lease renew 无法处理,导致幽灵成员或错误 takeover。
8. Room owner 与 generation fence
多 Relay pod 下,一个 Huddle session 由某 node/runtime 持有 lease。Redis directory 记录 owner 与 generation:
- renew 只接受当前 owner/generation。
- takeover 产生更高 generation。
- 旧 owner 的晚到帧/释放请求被 fence 拒绝。
- Mesh tunnel 每次发送可检查 lease 是否仍有效。
这是典型 fencing token:TTL 只说明租约可能过期,generation 才阻止旧持有者“复活”。
9. Desktop 音频栈
Desktop native backend 处理设备、jitter、playout、预处理、重连、PTT、STT/TTS 与 Agent TTS routing。前端只接收状态和用户控制,不承担实时 DSP。
本地 TTS 模型/voice registry 还有升级与选择测试;防睡眠确保会话/Agent 讲话时 OS 不进入不合适状态。
10. 生命周期事件
Huddle start/join/leave/end/archive 使用事件对协作层可见;高频音频帧不进入事件数据库。最后一个 peer 离开触发 teardown/end,异常断连靠 lease/timeout 收敛。
这是“控制面持久可观察、数据面实时短命”的分离。
11. 源码入口
crates/buzz-media/src/:Blossom、S3、验证、缩略图/blurhash。crates/buzz-relay/src/api/media.rs:HTTP/tenant/auth 接线。crates/buzz-relay/src/audio/handler.rs:Huddle wire handler。crates/buzz-relay/src/audio/room.rs:房间与 peer 控制。crates/buzz-relay/src/audio/join.rs:租约、admission、teardown。crates/buzz-relay/src/audio/mesh.rs:跨 node fence/tunnel。desktop/src-tauri/src/huddle/:客户端音频、STT/TTS 与重连。