GuidePlayback
Sync content with timed metadata
Show quizzes, product cards, ad markers, chapters, and transcripts in time with playback, from cues in the media or cues your app adds.
Timed metadata is data tied to a span of playback: a quiz question, a product card, an ad break, or a chapter. Video.js reads it as text tracks and cues, the platform objects captions use, whether the cues come from the media or from your app.
Recommended approach
Define the track with defineTextTrack(), add your cues to the player with useCreateTextTrack(), and render the cue useActiveTextCues() returns.
In your app, the cues usually come from your API or CMS. Fetch them and pass them as cues, or pass the file’s URL as src and the player loads it.
How it works
A text track has a kind, label, and language, and a list of cues. Each cue spans startTime to endTime, in seconds, and carries text, structured data in value, or both. value and type are named after the platform’s DataCue. A cue without an endTime is a point, active for one second, except in a chapters track your app adds, where it runs to the next chapter.
addTextTrack() adds the track to the player, not the media, so it works with any media, including YouTube and other embeds. The track sits next to the media’s tracks in textTrackList with source: 'app'.
The text cues feature, included in the video, audio, and live presets, reads cues from the media’s metadata and chapters tracks and from the tracks your app adds, and tracks which cues are active at the current time. Tracks from either place are read the same way.
defineTextTrack<Note>() types each cue’s value, and works as a filter: reading through it picks the tracks with the same kind, label, and language. A plain filter, such as { label: 'notes' }, works too, but its cues’ value is unknown.
useCreateTextTrack() adds the track while the component is mounted and removes it on unmount. useActiveTextCues() re-renders the component as cues start and end.
Browsers never draw metadata or chapters cues, so the overlay, its styles, and its accessibility are up to your UI.
Availability and constraints
- Active cues follow
currentTime, which updates up to about four times a second. Don’t rely on them for frame-accurate timing. A cue that playback passes between two updates still shows up as active once; a cue a seek jumps over doesn’t. cuesare plain, JSON-serializable data, so a server-rendered page can pass them in. Asrcis fetched only once the player runs on the client, and a cross-origin file needs CORS headers.- Browsers load a
<track>element’s cues only while the track isshowingorhidden.defaultmakes a<track kind="metadata">hidden; without it, the track staysdisabledand its cues never load. valueisn’t checked at runtime unless the definition has a schema. See Check values with a schema.
Like a <track> element, an app track stays through source changes until it’s removed. useCreateTextTrack() removes its track when the component unmounts, so give the component a key per video to swap tracks with the video.
Common variations
Read cues from a WebVTT file
Add a <track kind="metadata"> to the media. Cue text that parses as a JSON object or array becomes the cue’s value; any other text stays in text.
A definition with the same kind and label reads it, so the rendering code from the recommended approach works unchanged. To use the file with media that has no <track> support, such as an embed, pass it as src when you add the track instead.
Read metadata from an HLS stream
When hls.js plays a stream, in HlsJsVideo and media built on it such as MuxVideo, it publishes ID3 frames and EXT-X-DATERANGE tags as cues on a metadata track. Read them with hlsMetadataTrack. Each cue’s type tells an ID3 frame from a date range attribute, and narrows its value.
Where the browser plays HLS natively, as Safari does, in-band metadata arrives as DataCues on the browser’s own metadata tracks. Read those with a { kind: 'metadata' } filter.
Mark cues on the time slider
Add TimeSlider.Markers to the time slider to draw a marker for each cue on the tracks a filter matches, such as the notes from the recommended approach.
A point is a marker with no width, a range spans its cue, and a marker has data-active while its cue is active. Markers are unstyled: position each one with its --media-slider-marker-* custom properties. See the markers reference for the full styling contract.
Check values with a schema
Cues from a file or a stream can carry anything. Pass a schema to defineTextTrack() to check each value where it’s read. Any Standard Schema works, such as one from zod or valibot, and the value type comes from the schema.
Readers skip cues whose value fails the schema, and warn in development. The schema must be synchronous.
Add chapters
Add a chapters track the same way. A chapter’s title is its cue text, and a chapter without an endTime runs to the next one. When the media has no chapters track of its own, the time slider shows these chapters.
To load a Podcasting 2.0, Apple HLS, or JSON-LD chapters file, pass its converter as format, such as podcastChapters. Read the current chapter, with any artwork the file gives it, through chaptersTrack.
Show an interactive transcript
A transcript reads captions cues, which the text cues feature skips unless something asks for them.
useTextCues() and useActiveTextCues() ask for the tracks they match while the component is mounted.
The browser loads a <track> element’s cues only while captions are on. To show the transcript either way, add the captions track from your app instead of a <track> element. The player copies it into the media, so the browser still draws the captions when a viewer turns them on.
Troubleshooting
No cue is ever active
Confirm that:
- The track is one the text cues feature reads: a
metadataorchapterstrack that isn’tdisabled. Give a<track kind="metadata">thedefaultattribute. For other kinds, read through the hooks or callwatchTextCues(). - Your filter or definition matches the track’s
kind,label, andlanguageexactly.getTextTracks()lists the tracks the player has. - Cue times are in seconds.
A cue’s value is undefined
Only metadata cue text that parses as a JSON object or array becomes value; other text stays in text. With a schema, cues whose value fails it are skipped, with a warning in development.
An app track outlives its video
App tracks stay until they’re removed, through source changes. useCreateTextTrack() removes its track when the component unmounts, so give the component a key per video.
Cues from a src never load
The file failed to load, often because of a missing CORS header or a wrong URL. The track keeps any cues it already had, and a warning shows in development.
Related pages
Components
API
- Text cuesCues and active cues from metadata, chapters, and app text tracks
- defineTextTrackDefine a text track once, with the type of its cue values, to add and read it with
- useCreateTextTrackHook that adds a text track to the player while the component is mounted
- useActiveTextCuesHook that returns the cues active now on the text tracks a filter or track definition matches
- useTextCuesHook that returns every cue on the text tracks a filter or track definition matches
- getActiveTextCuesThe cues active now on the text tracks a filter or track definition matches
- getTextCuesEvery cue on the text tracks a filter or track definition matches
- chaptersTrackA track definition for chapters, and converters that turn chapter JSON formats into cues
- hlsMetadataTrackA track definition for the ID3 and EXT-X-DATERANGE metadata hls.js publishes