md-platform

VodChatReplay.md
View raw Back to list

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

Query params

ResponseListMessageDto[], ordered ascending by timestamp:

[
  {
    "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.
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

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

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

Switching from live to VOD chat

In the player's chat component, branch on video.type:

No state needs to migrate between the two — when a stream ends and the page reloads, the player simply mounts the replay flow.