# T4.6 — SFU keyframe cache **Status:** Pending Review **Agent:** Kimi Code CLI **Started:** 2026-05-12T16:29Z **Completed:** 2026-05-12T16:40Z **Commit:** **PRD:** ../PRD-video-v1.md ## What I changed - `crates/wzp-relay/src/room.rs:384-403` — Added `KeyframeCacheEntry` and `KeyframeBuffer` structs; `KeyframeCacheEntry` stores a complete keyframe's packets, sequence, timestamp, and byte size. - `crates/wzp-relay/src/room.rs:411-412` — Added `keyframe_cache` and `keyframe_buffer` `DashMap`s to `RoomManager`. - `crates/wzp-relay/src/room.rs:435-438, 447-450` — Initialized new fields in `new()` and `with_acl()`. - `crates/wzp-relay/src/room.rs:648-719` — Added `update_keyframe_cache()`: buffers keyframe packets per `(room, sender, stream)`; on `FLAG_FRAME_END` moves the buffer to `keyframe_cache`; on non-keyframe packets flushes stale partial buffers; enforces 200 KB per-stream cap. - `crates/wzp-relay/src/room.rs:721-734` — Added `cached_keyframes_for_room()` to retrieve all completed keyframes for replay. - `crates/wzp-relay/src/room.rs:736-742` — Added `clear_keyframes_for_room()` called from `leave()` when a room becomes empty. - `crates/wzp-relay/src/room.rs:530` — `join()` now returns `Vec>` of cached keyframes as the fourth tuple element. - `crates/wzp-relay/src/room.rs:550` — `join_ws()` updated to unpack the new return element. - `crates/wzp-relay/src/room.rs:943-944, 1201-1202` — Both `run_participant_plain` and `run_participant_trunked` call `update_keyframe_cache()` on every received media packet. - `crates/wzp-relay/src/main.rs:1939-1951` — After `join()`, cached keyframes are sent to the new participant via `transport.send_media()` before the RoomUpdate broadcast. ## Why these choices 1. **DashMap instead of `Room` lock** — The forwarding hot-path already acquires a read lock on the room for `others()`. Adding cache writes inside that lock would serialize all forwarding loops. Using separate `DashMap`s for cache and buffer avoids any room-lock contention. 2. **Two-phase buffering (pending → completed)** — A keyframe can span multiple packets (H.264 access units). We accumulate in `keyframe_buffer` until `FLAG_FRAME_END`, then atomically promote to `keyframe_cache`. Non-keyframe packets flush the pending buffer to prevent storing partial frames. 3. **Return keyframes from `join()`** — `join()` is synchronous, so it can't `await` sends. Returning the packets lets the async caller in `main.rs` replay them before broadcasting `RoomUpdate`, ensuring the new participant receives keyframes before live traffic. ## Deviations from the task spec The task spec in TASKS.md is a skeleton ("Skeleton — expand before claiming."). Implementation follows the PRD-video-v1 SFU keyframe cache section and adapts it to the existing relay architecture. ## Verification output ```bash $ cargo build -p wzp-relay Compiling wzp-relay v0.1.0 Finished `dev` profile [unoptimized + debuginfo] target(s) in 12.24s ``` ```bash $ cargo test -p wzp-relay running 20 tests ... (all pass) test result: ok. 20 passed; 0 failed; 0 ignored ``` ```bash $ cargo test --workspace --exclude wzp-video # 656 tests passed ``` ```bash $ cargo fmt --all -- --check # pass ``` ## Test summary - Tests added: 0 (keyframe cache is stateful and best verified by integration tests; the existing relay tests exercise join/leave paths) - Tests modified: 0 - Workspace test count: 656 pass - `cargo clippy -p wzp-relay --all-targets -- -D warnings`: pass (1 dead_code warning suppressed on `KeyframeCacheEntry` — fields are intentionally retained for future metrics) - `cargo fmt --all -- --check`: pass ## Risks / follow-ups 1. **No integration test yet** — A full test would need a mock `QuinnTransport` that injects keyframe-flagged packets, then asserts a late joiner receives them. This is deferred until the video pipeline is fully wired end-to-end. 2. **Keyframe cache not yet wired for WebSocket participants** — `join_ws()` discards cached keyframes (`_keyframes`). When WebSocket video receive is implemented, the caller should replay them. 3. **Per-sender cleanup on participant leave** — Currently only full-room emptying clears keyframes. Individual sender leave doesn't purge their cached keyframes; they are naturally overwritten by newer keyframes or removed when the room closes. ## Reviewer checklist (filled in by reviewer) - [ ] Code matches PRD intent - [ ] Verification output is real (re-run if suspicious) - [ ] No backward-incompat surprises - [ ] Tests cover the new behavior - [ ] Approved