跳转到内容

实现契约

这页记录跨模块正确性依赖的实现契约。修改 HTTP/gRPC handler、Provider 播放生成、播放 worker、读库路由、文件存储或 SQLx 查询前,先看这里。

HTTP 和 gRPC 是传输适配层。它们负责解析 path、query、JSON/protobuf body、headers、状态码和 streaming body,然后调用 synctv-api/src/impls

共享行为放在 implssynctv-core

  • 权限和 actor 校验;
  • 播放状态变更和资源观察;
  • Provider 调用、缓存、URL 过期和生命周期 fanout;
  • 聊天、文件、房间、媒体和直播业务规则;
  • 上传/下载 stream 处理。

Protobuf request type 是共享参数契约。HTTP handler 可以先把 path 或 query 中的字段填入 request,再调用 impls。gRPC handler 直接把 protobuf 值传入同一条 impl 路径。传输层新增代码只负责解析、编码、metadata 提取和 stream 适配。

MediaProvider::generate_playback 是 Provider 播放决策边界。每个 Provider 自己决定返回的所有 mode,包括 upstream/direct mode、proxy_* sibling、default_mode、headers、字幕、danmaku、thumbnail、manifest、过期时间、缓存 metadata 和直播资源生命周期数据。

Provider 签名在生成 playback 时完成。共享 helper 只做机械的 proxy_* sibling URL 生成;Provider 代码决定何时调用 helper、哪些 header 暴露给客户端、哪个 mode 作为默认值。

VersionedPlayback 是缓存 payload 和 proxy 查找索引。version 用于把 Provider 生成的 /stream/m3u8/mpd、字幕、danmaku、thumbnail、FLV 和 HLS segment URL 解析回同一个播放结果。Response finalization 只把当前请求的 signing key、room/user binding、version 和 expiry 交给 Provider 的 rewrite callback;mode、默认值、header 暴露、manifest metadata 和直播生命周期仍由该 Provider 决定。

App 客户端通常可以消费 provider 原生 URL,因此 upstream/direct mode 和 proxy sibling 同时可用时,Provider 应同时返回两类 mode。Provider 按 source 选择最稳妥的默认 mode:带 header 的 HLS 或 DASH 可以默认使用 proxy_*,简单直连文件可以默认使用 upstream URL。

Proxy 生成和 proxy 解析是同一个契约。Provider 只要生成 /stream/m3u8/mpd、字幕、danmaku、thumbnail、FLV、HLS playlist 或 HLS segment URL,就必须在同一个 Provider 中实现并测试对应 resolver。缓存命中和刷新生成的 playback 需要保持相同可用的 URL 形态。

Provider 返回内容的维护契约:

ProviderPlayback 内容契约
Direct URL返回 upstream mode;可代理时同时返回 proxy_* mode;带 header 的源默认走 proxy;HLS proxy manifest 要重写 segment URL;Range 请求需要真实验证。
Alist返回 upstream/direct 或转码 mode;同时返回 proxy_* sibling;默认 mode 由 Provider 按客户端能力选择;thumbnail、subtitle、HLS segment 和 stream proxy action 必须都有 resolver。
Emby/Jellyfin返回 upstream/transcode mode 和 proxy sibling;upstream token header 按产品策略允许给客户端;proxy sibling 必须覆盖 direct stream、HLS/transcode 和字幕。
Bilibili匿名可播放内容默认给出可用 direct/proxy mode;DASH/MPD 默认使用 proxy_* manifest;proxy manifest 的 segment URL 继续走 SyncTV;字幕、danmaku、thumbnail、缓存 metadata 和 CDN header 由 Bilibili Provider 自己维护。
RTMPPlayback URL 指向 SyncTV 管理的 HLS/FLV provider-proxy action;publish key、stream info、HLS playlist/segment、FLV 和 idle cleanup 属于同一生命周期。
live proxy外部 RTMP/HTTP-FLV 在首个 viewer 请求时拉流并注册为本节点 publisher;HLS/FLV URL 指向 SyncTV;idle cleanup 负责断开外部拉流并注销 publisher。

新增 mode 的规则:生成出来的每个 URL 都必须有对应 resolver;缓存命中、缓存失效、URL 过期、headers、manifest/segment/subtitle/danmaku/thumbnail 都要在同一个 Provider 内维护。

Provider 手动验证覆盖完整 PlaybackResult

  • Direct URL、Alist、Emby、Jellyfin、Bilibili 匿名播放、RTMP 和 live proxy;
  • 返回给客户端的每个 playback mode 和每个 URL;
  • direct URL、proxy URL、HLS/DASH manifest、indexed segment、字幕、danmaku、thumbnail、FLV、Range 请求、缓存命中/未命中、过期和清理;
  • 用构建出的 synctv 二进制和 CLI 搭环境,再用 curl 请求返回的 URL;
  • 动态 playlist 路径列表、媒体解析、播放生成、target 切换、封面/thumbnail 路由和自动切换后的 target 变化;
  • RTMP 和 live proxy 完整生命周期:通过 CLI 创建,发布或拉取真实上游,请求 stream info、HLS playlist/segment、FLV,断开连接,然后验证 idle cleanup。

需要手动验证的工作流,先补 synctv CLI,再通过 CLI 加 curl 验证。

播放后台任务使用当前进程的 active rooms 作为调度输入。Active 指本节点 ConnectionRuntime 中至少有一个 Realtime 连接。

这个契约用于时长探测、自动切换和播放资源生命周期任务。Worker 在每个节点运行。同一房间可以在多个节点 active,重复尝试由持久化层收敛:

  • 时长探测 claim 使用数据库锁和 SKIP LOCKED
  • 自动切换使用播放状态事务和乐观版本;
  • 动态 playlist 任务连接 room_playback_progress.target_hash,让任务绑定当前选中的 target。

这些 worker 使用 ConnectionRuntime::active_room_ids()。Presence 和 hot-room 统计用于列表、管理视图、分析和 metrics。Leader election 用于分区维护、清理等全局单例任务。

时长探测查询需要同时保留 state.room_id = ANY($active_room_ids)progress.target_hash = metadata.target_hash。前者保持本进程 active-room 调度边界,后者把动态 playlist 探测绑定到当前播放 target。

自动切换需要通过播放状态事务和 version 写入推进状态。多个节点同时扫描同一个 active room 时,最终只能有一次状态推进成功。

有限 sequential playlist 到达末尾时,自动切换需要通过同一条状态写入路径持久化稳定的结束状态。持久化后的结束状态让后续扫描周期跳过已经结束的 source。

读库按 repository method 显式选择。只有白名单内、允许最终一致的列表/发现类读取走 DatabasePools::read()

这些操作使用主库:

  • 写入、事务、migration 和 status 检查;
  • 认证和授权检查;
  • 会暴露过期凭据的 cache prewarm 和连接构建输入;
  • 写入后的通知 fanout;
  • cursor 来自主库的 snapshot-plus-cursor 读取;
  • 播放 worker claim 和状态变更;
  • 文件上传/下载 finalize 和 ownership 校验。

新增读库查询时,需要写明 replica lag 可接受的原因,并让 response cursor、cache 写入和副作用使用一致的数据来源。

文件存储有一个对象注册表,以及独立的字节表和会话表:

  • file_objects 是对象注册表。validated_at 表示对象已经可用,NULL 表示 pending session placeholder。
  • file_blob_parts 保存 database 后端对象字节。Pending upload 先把 part 写到 upload_session_key,finalize 时用 UPDATE 把这些行提升到最终 object_key
  • file_upload_sessions 保存 database 与 S3 的 single-object 或 multipart 上传状态。
  • file_upload_session_parts 保存已上传 part 进度,以及 provider ETag/checksum。

Database multipart 上传在写入每个 part 时校验该 part 的 SHA-256。Finalize 使用已保存的 part checksum 重新计算 manifest digest,然后用 UPDATEfile_blob_parts.object_keyupload_session_key 提升到最终 object_key。Pending bytes 和 final bytes 使用同一张 part 表,通过 object key 表达状态,resume/idempotency 校验开销较低。

Single-object 上传 session 仍使用服务端决定的 manifest plan,但通过一次 PUT 写入完整对象,并在 complete 时不写 part 进度行。S3 multipart session 返回客户端直传 S3 的 presigned part URL。服务端代理上传/下载路径作为需要部署兼容时的能力保留。Database 后端的 URL 指向 SyncTV,因为字节存储在应用背后的数据库里。

下载是流式的。HTTP 返回普通二进制响应,body 由服务端 stream 驱动;gRPC 返回 protobuf chunk stream。Core service 返回 FileObjectDownload,让两种传输共享同一个 storage 实现,主路径按 chunk 推进对象读取。

上传 part size 由服务端规划。修改规划影响新 session;已打开的 session 使用自己保存的 part_size_bytes 和 manifest。校验 part 时读取 session 保存的 plan,保证中途调整默认值时已有 session 继续按原 manifest resume。

Repository 代码使用 SQLx checked macros。保留 checked macro 做查询形状校验,SQL 变化时同步更新 .sqlx

Terminal window
cargo sqlx prepare --workspace -- --all-targets
SQLX_OFFLINE=true cargo check --workspace --all-targets

SQL 或查询返回列变化时,.sqlx metadata 属于同一个改动。

需要 compile-time SQL 校验的 repository 查询继续使用 checked macros。更新 .sqlx 是正确做法;为了绕开 cache 改成 unchecked query 会丢掉离线 CI 对查询参数、返回列和 nullability 的保护。

播放 worker SQL 变化还需要 active-room E2E 证据:没有本节点 WebSocket 时不探测 metadata,有本节点 WebSocket 时探测 metadata,动态 playlist 使用 target-hash 绑定,多个 worker 能看到同一房间时只提交一次自动切换。