# VOD Chat Replay — Frontend Integration When a livestream ends and the video becomes a VOD, the player should replay the chat in sync with playback. This document explains how to consume the replay endpoint and drive it from the player. ## Endpoint ``` GET /videos/{id}/messages/replay?from={long}&to={long} GET /mobile/videos/{id}/messages/replay?from={long}&to={long} ``` **Path params** - `id` — the encoded video id (same id used elsewhere in the API). **Query params** - `from` — inclusive lower bound on `Message.Timestamp`. - `to` — exclusive upper bound on `Message.Timestamp`. **Response** — `ListMessageDto[]`, ordered ascending by `timestamp`: ```json [ { "id": "01HX...", "user": { "id": "u_123", "name": "alice", "nickname": "Alice" }, "content": "lol", "timestamp": 1717245001234, "createdAt": "2026-06-01T12:30:01.234Z" } ] ``` Returned chunks are immutable and cached server-side for 7 days, so identical `(id, from, to)` calls are cheap. ## Timestamp model `Message.Timestamp` is the absolute timestamp the message was sent during the live stream (same unit it was originally recorded in — pass it back to the API as-is, do not convert). To map the player's `currentTime` (seconds since stream start) to a `Timestamp`, you need the stream's reference start. Use one of: 1. **`liveStartDate` from the video DTO** (`VideoStreamDetails.LiveStartDate`) — convert to the same unit as `Message.Timestamp` and add `currentTime`. 2. **The first message's timestamp** — if you don't have `liveStartDate`, fetch a tiny initial chunk and treat the lowest `timestamp` as your origin. ```ts const streamStartMs = new Date(video.streamDetails.liveStartDate).getTime(); const playbackToTimestamp = (currentTimeSec: number) => streamStartMs + Math.floor(currentTimeSec * 1000); ``` > Adjust `* 1000` if the backend stores `Timestamp` as seconds rather than milliseconds — confirm against a sample row before shipping. ## Replay loop The replay should pull chat in small chunks ahead of the playhead and emit messages to the UI as the player crosses each timestamp. ### Recommended parameters | Param | Suggested value | Why | |---|---|---| | `CHUNK_SECONDS` | `30` | One request covers ~30s of chat | | `PREFETCH_AHEAD_SECONDS` | `10` | Fetch the next chunk this far before exhausting the buffer | | `MAX_BUFFER_SECONDS` | `60` | Stop prefetching past this horizon | ### Pseudo-implementation ```ts class VodChatReplay { private buffer: ListMessage[] = []; private fetchedUntil = 0; // last `to` we've fetched, in Timestamp units private lastEmittedIndex = -1; constructor( private videoId: string, private streamStartMs: number, private api: PicTvApi, ) {} // Call on play / seek / when the chunk runs out. async ensureBuffered(currentTimeSec: number) { const head = this.streamStartMs + currentTimeSec * 1000; const horizon = head + MAX_BUFFER_SECONDS * 1000; while (this.fetchedUntil < horizon) { const from = Math.max(this.fetchedUntil, head); const to = from + CHUNK_SECONDS * 1000; const chunk = await this.api.getReplayMessages(this.videoId, from, to); this.buffer.push(...chunk); this.fetchedUntil = to; } } // Call on every `timeupdate` from the player. tick(currentTimeSec: number, onMessage: (m: ListMessage) => void) { const head = this.streamStartMs + currentTimeSec * 1000; // Top up the buffer when we're getting close to the end. if (this.fetchedUntil - head < PREFETCH_AHEAD_SECONDS * 1000) { this.ensureBuffered(currentTimeSec); } // Emit any buffered messages whose timestamp has now passed. while ( this.lastEmittedIndex + 1 < this.buffer.length && this.buffer[this.lastEmittedIndex + 1].timestamp <= head ) { this.lastEmittedIndex += 1; onMessage(this.buffer[this.lastEmittedIndex]); } } // Call on seek (forward or backward). reset() { this.buffer = []; this.fetchedUntil = 0; this.lastEmittedIndex = -1; } } ``` ### Wiring to the player ```ts const replay = new VodChatReplay(video.id, streamStartMs, api); player.on('play', () => replay.ensureBuffered(player.currentTime)); player.on('timeupdate', () => replay.tick(player.currentTime, (m) => chatStore.append(m)), ); player.on('seeking', () => { chatStore.clear(); replay.reset(); }); ``` ## Edge cases - **Seek backward** — clear the chat UI and reset the replay buffer; let it refetch. - **Seek far forward** — same: reset, then `ensureBuffered` at the new position. - **Pause** — keep the buffer; tick will resume naturally on play. - **Playback rate > 1×** — `tick` is timestamp-driven, so messages still emit in order; you may want to drop emits if more than N messages fall in one tick. - **Empty chunks** — normal during quiet stretches; the loop still advances `fetchedUntil`, so no extra work. - **End of stream** — when `from` exceeds `liveEndDate`, subsequent calls return `[]`. Stop calling once `head > liveEndDate`. ## Switching from live to VOD chat In the player's chat component, branch on `video.type`: - `Livestream` → existing websocket / `GET /videos/{id}/messages` flow. - `Vod` → instantiate `VodChatReplay` instead. No state needs to migrate between the two — when a stream ends and the page reloads, the player simply mounts the replay flow.