Skip to content

Closed captions

Closed captions (CC) travel inside the video itself rather than as a separate track: in per-frame side data (SEI for H.264 and HEVC, user data for MPEG-2, metadata OBU for AV1). TV sets and set-top boxes understand them, but a browser player cannot show such text: HLS and DASH expect subtitles as a separate track.

Sapsan does both: it passes captions through inside the video as they are, and it decodes them into a WebVTT subtitle track.

Processing modes

The mode is set by the mode field of the stream's closed_captions section. It controls delivery only — decoding and archive recording always happen:

  • passthrough (default) — captions stay inside the video and are announced in manifests as closed captions. There is no subtitle track in the manifests.
  • extract — a subtitle track instead of the embedded captions: the text is decoded into a WebVTT track, while the captions are cut out of the outgoing video and are not announced as closed captions.
  • both — both at once: captions travel inside the video and are announced, and a subtitle track goes alongside them.

Configuration

streams:
  - name: news
    inputs:
      - url: udp://239.0.0.1:1234
    closed_captions:
      mode: extract
      services:
        CC1:
          language: eng
          name: English
        SERVICE3:
          language: spa
          name: Espanol

The services section is optional and does two things:

  • sets the name and language of the player's menu entry. Without it the name is derived from the stream: the language announced by the service, otherwise the service address (CC1, SERVICE3);
  • pre-announces the service in extract and both modes: a listed service gets its track from the very first segment, without waiting for the first cue. This matters for players that read the track list once at start-up: a service that starts speaking ten minutes in would otherwise appear in the menu only after the stream is reopened.

The service key is CC1CC4 for CEA-608 and SERVICE1SERVICE63 for CEA-708.

What the player gets

Every speaking service becomes its own text track with a fixed number: CC1CC4t1t4, SERVICE1SERVICE63t5t67. The number belongs to the service and does not change when the stream restarts, so links to a track keep working.

In HLS the track arrives as an EXT-X-MEDIA rendition with TYPE=SUBTITLES, in DASH as an adaptation set with contentType="text" and mimeType="text/vtt". Track segments live at addresses like:

/streaming/v/news/subtitles/t1/1738245600000.vtt

The last path segment is the start of the window in milliseconds; the end of the window is decided by the server from the same segment grid the video uses. An empty window is a valid empty WebVTT segment, not an error: the track must not break during a pause between cues.

Subtitles are served live, in rewind and from the archive. Decoded cues are always written to the archive regardless of the mode, so switching extract on works retroactively: subtitles show up for the already recorded archive as well.

The extract mode limitation on TS outputs

In extract mode captions are cut out of the outgoing video entirely — on every output at once, MPEG-TS included: udp/rtp/srt push, .ts HLS segments and tshttp. A text track has nothing to travel in inside MPEG-TS, so a consumer of a TS output is left with no subtitles at all in this mode.

If the same stream is watched in a browser and pulled over MPEG-TS at the same time, use both: it keeps the captions inside the video for the TS consumer and adds the track for the browser.

Migrating from Flussonic Media Server

In legacy Flussonic the cc.extract option did not remove the captions from the video — it enabled extraction in addition to the embedded ones. Its direct equivalent in Sapsan is therefore mode: both, not mode: extract.

Flussonic Sapsan Behaviour
option not set mode: passthrough captions inside the video, no track
cc.extract mode: both captions inside the video and a subtitle track
mode: extract new strict behaviour: a track instead of the embedded captions

mode: extract is new behaviour with no legacy Flussonic counterpart. Choose it when none of your consumers need the embedded captions: it removes the duplicate menu entry in the player (the same subtitles both as closed captions and as a track) and saves space in the video frames.

Verification

To confirm that the track has appeared, check the master playlist:

curl -s http://localhost:8080/streaming/v/news/index.m3u8 | grep SUBTITLES

and the segment content:

curl -s http://localhost:8080/streaming/v/news/subtitles/t1/1738245600000.vtt

The detected services and each one's track number are visible in the stream statistics (GET /streaming/api/v4/streams/stats/news, the captions section).

The pipeline itself is reported by Prometheus metrics prefixed with stream_captions_: stream_captions_cc_pairs_total shows whether captions reach the server at all, stream_captions_cues_decoded_total how many cues were decoded, stream_captions_cues_delivered_total how many made it into the track. A growing stream_captions_unrecognized_payloads_total means the stream carries a caption wrapper we do not know; report it to support.