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
extractandbothmodes: 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 CC1…CC4 for CEA-608 and SERVICE1…SERVICE63 for CEA-708.
What the player gets¶
Every speaking service becomes its own text track with a fixed number: CC1…CC4 → t1…t4, SERVICE1…SERVICE63 → t5…t67. 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.