The playback tracker records each attempt to watch a video as a view. Each view has a view_id, and its events have their own event_id and sequence number within that view.
In each payload, event_name includes the chunkify.video. prefix.
Use includeEvents to choose which event types reach your destinations. With Chunkify Player already on the page, this example sends only the start and final records for each view:
Use short names such as view_ended, without the chunkify.video. prefix. Omit includeEvents to send all nine types. An empty array sends nothing.
The selection applies to every destination. The tracker still measures playback when intermediate events are excluded, so view_ended retains its view metrics. Call session.destroy() when monitoring stops.
Shared fields
The payloads below show complete, illustrative tracker events before destination mapping. Measurements are null when unavailable; zero means a measured zero.
Every event contains these fields:
Fields
Meaning
schema_version
Event schema version. Currently 1.
event_name
Full event name, such as chunkify.video.view_started.
event_id
Unique record ID. Use it to deduplicate deliveries.
sequence
Increasing sequence number within the view.
occurred_at
UTC timestamp when the tracker emits the record.
view_id
ID for this playback attempt.
video_id
The videoId supplied by your application.
sdk_name, sdk_version
Playback package name and version.
connector_name, connector_version
Media or engine connector that produced the event.
player_name, player_version
Player reported by the connector, or null. This can identify the underlying engine, such as hls.js, when using Chunkify Player.
visibility_state
visible, hidden, or null when tab visibility is unavailable.
delivery_hostname
Media hostname when available. The complete playback URL is not included.
environment
Your optional environment label, or null.
viewer_id
Anonymous ID the tracker generates for the browser. It is reused across page loads unless persistViewerId is false.
user_id
Your optional userId, or null.
video_title
Your optional videoTitle, or null.
reason, startup_outcome
End reason and startup result on view_ended. Otherwise null.
view_started opens a viewing attempt. It normally follows the media element's play event. The Shaka connector starts the attempt when Shaka loads a source.
It is the only event that carries your custom dimensions, as a custom object with the keys and values you set. The field is omitted when you set none.
A start does not guarantee that any video played. If your code calls play(), call session.startAttempt() immediately beforehand to include attempts rejected before a play event.
The payload includes all view metrics, with initial counters and measurements available at that time.
view_snapshot reports cumulative view metrics. By default, the tracker sends a snapshot after each 30 seconds of active playing time. Pauses, buffering, and seeking do not advance that interval.
A tab visibility change also emits a snapshot during an attempt. A hidden tab can keep accumulating playing time; visibility does not determine whether playback is paused.
Set snapshotIntervalMs in your collection options to change the interval. Use the latest totals or differences between snapshots when calculating usage.
playback_state_changed reports a transition between playback states. Its payload adds these fields to the shared fields:
Field
Meaning
from_state
State the view left.
to_state
State the view entered.
previous_state_duration_ms
Wall-clock milliseconds spent in from_state.
playhead_seconds
Media position when the transition is recorded, or null.
Possible states are startup, resuming, playing, paused, buffering, seeking, error, ended, and stopped.
When seek_completed is enabled, it replaces intermediate state-change records during a seek interaction. A final transition to ended or stopped normally precedes view_ended.
rendition_changed reports the initial known rendition and later changes. The initial selection has null previous-rendition fields. Repeated reports of the same rendition do not emit another record.
The connector must expose rendition information. Initial selection is not counted as a rendition switch.
Fields
Meaning
playhead_seconds
Media position at the change, or null.
player_width_px, player_height_px
Displayed media size in CSS pixels.
previous_rendition_*
Previous rendition's width, height, codec, bitrate_bps, and frame_rate.
selected_rendition_*
Selected rendition's width, height, codec, bitrate_bps, and frame_rate.
Rendition dimensions are pixels, bitrate is bits per second, and frame rate is frames per second. Individual properties can be null.
seek_completed summarizes a logical seek interaction. Rapid seeks are grouped until 400 ms have passed since the last seeked. If playback was running, the tracker also waits for it to resume.
Fields
Meaning
seek_started_at
UTC timestamp of the first seek.
last_seeked_at
UTC timestamp of the final seeked, or null.
playback_resumed_at
UTC timestamp when playback resumes, or null.
state_before_seek, state_after_seek
Playback states surrounding the interaction.
from_playhead_seconds, to_playhead_seconds
Starting and final media positions, or null when unavailable.
seek_interaction_duration_ms
Total time spent seeking during this interaction.
seek_resume_wait_ms
Time from the final seeked to resumed playback, or null.
seek_outcome
resumed, paused, or interrupted.
A view ending, a source change, or a fatal error can flush a pending seek as interrupted. Use the seek timestamps to locate the interaction; occurred_at is when its summary is emitted.
subtitle_changed reports a change of the showing subtitle or caption track during a view. The tracker reads the media element's text tracks, so it detects the language choices made in Chunkify Player's menus as well as tracks your code turns on or off.
Field
Meaning
previous_subtitle_language
Language before the change, or null when no subtitles were detected.
Media position when the change was recorded, or null.
The record is sent only when the speed changes after rounding. With Shaka Player, the tracker reports Shaka's playback rate, so buffering is not recorded as a speed change. The speed when a view starts is in view_started.
playback_error includes cumulative view metrics and the first reported error in that view. Later error reports in the same view do not produce additional error records.
Field
Meaning
error_category
aborted, network, decode, unsupported, or unknown.
error_code
Browser or engine code, as a string or number, or null.
error_fatal
Whether the reported error is fatal.
error_playhead_seconds
Media position when the error was recorded, or null.
Raw error messages and URLs are omitted. A fatal error moves the view into the error state; view_ended still reports how the attempt later closes.
view_ended closes an attempt and includes its final cumulative metrics. It can be sent when playback finishes, monitoring stops, the source changes, or the page exits.
Field
Values
reason
ended, destroyed, source_changed, or page_exit.
startup_outcome
started, failed_before_start, or ended_before_start.
Only reason: "ended" means playback reached the media end. startup_outcome: "started" means the tracker observed playback begin. An attempt can end before playback starts without a reported failure.
view_started, view_snapshot, playback_error, and view_ended include the metrics below. Time counters accumulate within a view; dimensions and rendition fields describe the current playback state.
State, rendition, seek, subtitle, and playback-rate records contain their event-specific fields instead.
Time and progress
Field
Meaning
playhead_seconds
Current media position, or null.
played_ranges
Merged media sections played by this attempt, or null when unavailable.
media_duration_seconds
Media duration, or null when unknown.
startup_time_ms
Time from the attempt starting to the first playing event.
first_video_frame_time_ms
Time to the first video frame callback when the browser exposes it. It can differ from startup time.
playing_time_ms
Wall-clock time spent playing, excluding pauses, buffering, and seeking.
rebuffer_count
Buffering interruptions after startup, excluding seeks.
rebuffer_duration_ms
Cumulative time spent buffering after startup.
rebuffer_ratio
Buffering time divided by playing plus buffering time. null until that total is positive.
seek_count
Number of grouped seek interactions.
seek_duration_ms
Cumulative time spent seeking.
maximum_playhead_seconds
Furthest media position reached.
furthest_progress_ratio
Furthest position divided by media duration, capped at 1. Seeking forward increases it without watching the intervening video.
Video and player
Fields
Meaning
media_width_px, media_height_px
The media element's intrinsic video dimensions. They can differ from encoded rendition dimensions.
player_width_px, player_height_px
Displayed media box in CSS pixels. For Chunkify Player, this is the inner media element.
dropped_video_frames, total_video_frames
Per-view browser frame counters, when available.
Player dimensions are sampled when records are emitted. The final event retains the last measurable size if the element has already been removed.
Cumulative playing time per known rendition. Each entry contains width, height, codec, bitrate_bps, frame_rate, and playing_time_ms.
delivery_request_count
Completed media requests observed by the connector.
delivery_transferred_bytes
Response bytes observed by the connector.
delivery_throughput_bps
Aggregate transfer throughput in bits per second.
rendition_playing_time is null without a rendition-aware connector and [] before one identifies a rendition. It excludes playing time before the rendition is known, so its total can be lower than playing_time_ms.
Engine metrics depend on the active connector. hls.js, Shaka, and Video.js 10 with its hls.js engine expose rendition and request data when available.
Native <video> and Chunkify Player's native HLS or MP4 playback leave engine metrics null.
Subtitles and playback speed
Fields
Meaning
subtitle_language
Language of the showing subtitle or caption track, such as fr or en-US. und means a track without a declared language. null means no subtitles were detected.
playback_rate
Current playback speed, rounded to two decimals, such as 1 or 1.5. null for a speed outside the reported range, such as 0.
subtitle_playing_time
Cumulative playing time per subtitle language, including null for time without subtitles. Each entry contains language and playing_time_ms. Only on view_snapshot and view_ended.
playback_rate_playing_time
Cumulative playing time per speed. Each entry contains rate and playing_time_ms. Only on view_snapshot and view_ended.
null subtitles means either subtitles are off or they cannot be detected. The tracker reads the media element's showing subtitles or captions track, so subtitles that a player draws without a showing text track, such as Shaka's built-in subtitle display, are reported as null. When several tracks show at once, the first one is reported.
The playing-time lists count only time spent playing, like rendition_playing_time. Each list keeps at most 32 distinct values; playing time in further values is not added, so a list's total can be lower than playing_time_ms. Playing time at a null speed is not attributed to a speed.
Watched sections and background playback
played_ranges records the sections actually played as cumulative start_seconds and end_seconds pairs. Overlaps and replays merge within an attempt; seeking over a section does not mark it watched.
An empty array means no measured section. null means coverage is unavailable, including after the tracker exceeds 128 disjoint ranges. Use the final record, or the latest snapshot, for watched-section reports.
visibility_state records the document visibility at event time. A pause recorded while hidden can be shown as "Paused in background", but it does not establish whether the browser, application, or viewer caused it.
Use events in reports
Count distinct view_id values from view_started to count attempts. To count confirmed playback starts, use the first transition to playing, or final records with startup_outcome: "started".
Do not add cumulative snapshots together. Use each view's final totals, or differences between successive snapshots. A final record can be lost when the page closes, so snapshots can provide an earlier result.
Deduplicate by event_id and order a view's records by sequence. Distinct viewer_id values approximate unique viewers per browser; each view_id identifies an attempt.
See Send events to choose destinations and transform the payload they receive.
# Events and metrics
> Playback event payloads and measurements.
The playback tracker records each attempt to watch a video as a view. Each view has a `view_id`, and its events have their own `event_id` and sequence number within that view.
In each payload, `event_name` includes the `chunkify.video.` prefix.
| Event | When it is sent |
| --- | --- |
| [`view_started`](#view-started) | A playback attempt starts. |
| [`view_snapshot`](#view-snapshot) | A cumulative update during playback. |
| [`playback_state_changed`](#playback-state-changed) | Playback enters a different state. |
| [`rendition_changed`](#rendition-changed) | The first known rendition is selected, or playback changes rendition. |
| [`seek_completed`](#seek-completed) | A grouped seek interaction settles or is interrupted. |
| [`subtitle_changed`](#subtitle-changed) | The showing subtitle or caption language changes during a view. |
| [`playback_rate_changed`](#playback-rate-changed) | The playback speed changes during a view. |
| [`playback_error`](#playback-error) | The first reported playback error in the view. |
| [`view_ended`](#view-ended) | The viewing attempt ends. |
## Choose events
Use `includeEvents` to choose which event types reach your destinations. With Chunkify Player already on the page, this example sends only the start and final records for each view:
```javascript
import '@chunkify/player';
import { monitor } from '@chunkify/player-analytics';
await customElements.whenDefined('chunkify-player');
const player = document.querySelector('chunkify-player');
if (!player) throw new Error('Missing chunkify-player element');
const session = monitor(player, {
videoId: 'ast_3Ljtoej7c3Y1plabZoElbQwdwLh',
destinations: [{ send: (event) => console.log(event) }],
includeEvents: ['view_started', 'view_ended'],
});
```
Use short names such as `view_ended`, without the `chunkify.video.` prefix. Omit `includeEvents` to send all nine types. An empty array sends nothing.
The selection applies to every destination. The tracker still measures playback when intermediate events are excluded, so `view_ended` retains its view metrics. Call `session.destroy()` when monitoring stops.
## Shared fields
The payloads below show complete, illustrative tracker events before [destination mapping](/docs/integration/analytics/send-events#map-event-fields). Measurements are `null` when unavailable; zero means a measured zero.
Every event contains these fields:
| Fields | Meaning |
| --- | --- |
| `schema_version` | Event schema version. Currently `1`. |
| `event_name` | Full event name, such as `chunkify.video.view_started`. |
| `event_id` | Unique record ID. Use it to deduplicate deliveries. |
| `sequence` | Increasing sequence number within the view. |
| `occurred_at` | UTC timestamp when the tracker emits the record. |
| `view_id` | ID for this playback attempt. |
| `video_id` | The `videoId` supplied by your application. |
| `sdk_name`, `sdk_version` | Playback package name and version. |
| `connector_name`, `connector_version` | Media or engine connector that produced the event. |
| `player_name`, `player_version` | Player reported by the connector, or `null`. This can identify the underlying engine, such as hls.js, when using Chunkify Player. |
| `visibility_state` | `visible`, `hidden`, or `null` when tab visibility is unavailable. |
| `delivery_hostname` | Media hostname when available. The complete playback URL is not included. |
| `environment` | Your optional environment label, or `null`. |
| `viewer_id` | Anonymous ID the tracker generates for the browser. It is reused across page loads unless `persistViewerId` is `false`. |
| `user_id` | Your optional `userId`, or `null`. |
| `video_title` | Your optional `videoTitle`, or `null`. |
| `reason`, `startup_outcome` | End reason and startup result on `view_ended`. Otherwise `null`. |
| `error_category`, `error_code`, `error_fatal`, `error_playhead_seconds` | Error details on `playback_error`. Otherwise `null`. |
## Events
### View started
`view_started` opens a viewing attempt. It normally follows the media element's `play` event. The Shaka connector starts the attempt when Shaka loads a source.
It is the only event that carries your [custom dimensions](/docs/integration/analytics#custom-dimensions), as a `custom` object with the keys and values you set. The field is omitted when you set none.
A start does not guarantee that any video played. If your code calls `play()`, call `session.startAttempt()` immediately beforehand to include attempts rejected before a `play` event.
The payload includes all [view metrics](#view-metrics), with initial counters and measurements available at that time.
<expandable title="view_started payload">
```json
{
"schema_version": 1,
"event_name": "chunkify.video.view_started",
"event_id": "d67dcfdb-3be0-5a25-8693-aad22c635eca",
"sequence": 1,
"occurred_at": "2026-10-22T12:00:00.000Z",
"view_id": "06883d69-3643-40bf-b6a0-9556674fdccf",
"video_id": "ast_3Ljtoej7c3Y1plabZoElbQwdwLh",
"sdk_name": "@chunkify/player-analytics",
"sdk_version": "0.11.0",
"connector_name": "hls.js",
"connector_version": "1.6.5",
"player_name": "hls.js",
"player_version": "1.6.5",
"visibility_state": "visible",
"delivery_hostname": "media.example.com",
"environment": "production",
"viewer_id": "3f0c6a1e-2b7d-4c59-9e1a-7d4b2f8c6e10",
"user_id": null,
"video_title": null,
"reason": null,
"startup_outcome": null,
"error_category": null,
"error_code": null,
"error_fatal": null,
"error_playhead_seconds": null,
"custom": {
"plan": "premium",
"ab_test": "B"
},
"media_duration_seconds": 120,
"playhead_seconds": 0,
"played_ranges": [],
"startup_time_ms": null,
"first_video_frame_time_ms": null,
"playing_time_ms": 0,
"rebuffer_count": 0,
"rebuffer_duration_ms": 0,
"rebuffer_ratio": null,
"seek_count": 0,
"seek_duration_ms": 0,
"maximum_playhead_seconds": 0,
"furthest_progress_ratio": null,
"media_width_px": 1280,
"media_height_px": 720,
"player_width_px": 640,
"player_height_px": 360,
"dropped_video_frames": 0,
"total_video_frames": 0,
"selected_rendition_width": null,
"selected_rendition_height": null,
"selected_rendition_codec": null,
"selected_rendition_bitrate_bps": null,
"rendition_switch_count": 0,
"rendition_playing_time": [],
"delivery_request_count": 0,
"delivery_transferred_bytes": 0,
"delivery_throughput_bps": null,
"subtitle_language": null,
"playback_rate": 1
}
```
</expandable>
### View snapshot
`view_snapshot` reports cumulative view metrics. By default, the tracker sends a snapshot after each 30 seconds of active playing time. Pauses, buffering, and seeking do not advance that interval.
A tab visibility change also emits a snapshot during an attempt. A hidden tab can keep accumulating playing time; visibility does not determine whether playback is paused.
Set `snapshotIntervalMs` in your [collection options](/docs/integration/analytics#configure-collection) to change the interval. Use the latest totals or differences between snapshots when calculating usage.
<expandable title="view_snapshot payload">
```json
{
"schema_version": 1,
"event_name": "chunkify.video.view_snapshot",
"event_id": "02fb4f0d-17ea-576f-ac50-ceae7701e16a",
"sequence": 6,
"occurred_at": "2026-10-22T12:00:31.500Z",
"view_id": "06883d69-3643-40bf-b6a0-9556674fdccf",
"video_id": "ast_3Ljtoej7c3Y1plabZoElbQwdwLh",
"sdk_name": "@chunkify/player-analytics",
"sdk_version": "0.11.0",
"connector_name": "hls.js",
"connector_version": "1.6.5",
"player_name": "hls.js",
"player_version": "1.6.5",
"visibility_state": "visible",
"delivery_hostname": "media.example.com",
"environment": "production",
"viewer_id": "3f0c6a1e-2b7d-4c59-9e1a-7d4b2f8c6e10",
"user_id": null,
"video_title": null,
"reason": null,
"startup_outcome": null,
"error_category": null,
"error_code": null,
"error_fatal": null,
"error_playhead_seconds": null,
"media_duration_seconds": 120,
"playhead_seconds": 55,
"played_ranges": [
{
"start_seconds": 0,
"end_seconds": 10
},
{
"start_seconds": 35,
"end_seconds": 55
}
],
"startup_time_ms": 500,
"first_video_frame_time_ms": 520,
"playing_time_ms": 30000,
"rebuffer_count": 0,
"rebuffer_duration_ms": 0,
"rebuffer_ratio": 0,
"seek_count": 1,
"seek_duration_ms": 400,
"maximum_playhead_seconds": 55,
"furthest_progress_ratio": 0.4583333333333333,
"media_width_px": 1920,
"media_height_px": 1080,
"player_width_px": 640,
"player_height_px": 360,
"dropped_video_frames": 0,
"total_video_frames": 900,
"selected_rendition_width": 1920,
"selected_rendition_height": 1080,
"selected_rendition_codec": "avc1.640028",
"selected_rendition_bitrate_bps": 3000000,
"rendition_switch_count": 1,
"rendition_playing_time": [
{
"width": 1280,
"height": 720,
"codec": "avc1.64001f",
"bitrate_bps": 1500000,
"frame_rate": 30,
"playing_time_ms": 10000
},
{
"width": 1920,
"height": 1080,
"codec": "avc1.640028",
"bitrate_bps": 3000000,
"frame_rate": 30,
"playing_time_ms": 20000
}
],
"delivery_request_count": 20,
"delivery_transferred_bytes": 6000000,
"delivery_throughput_bps": 8000000,
"subtitle_language": "fr",
"playback_rate": 1,
"subtitle_playing_time": [
{
"language": null,
"playing_time_ms": 10000
},
{
"language": "fr",
"playing_time_ms": 20000
}
],
"playback_rate_playing_time": [
{
"rate": 1,
"playing_time_ms": 30000
}
]
}
```
</expandable>
### Playback state changed
`playback_state_changed` reports a transition between playback states. Its payload adds these fields to the shared fields:
| Field | Meaning |
| --- | --- |
| `from_state` | State the view left. |
| `to_state` | State the view entered. |
| `previous_state_duration_ms` | Wall-clock milliseconds spent in `from_state`. |
| `playhead_seconds` | Media position when the transition is recorded, or `null`. |
Possible states are `startup`, `resuming`, `playing`, `paused`, `buffering`, `seeking`, `error`, `ended`, and `stopped`.
When `seek_completed` is enabled, it replaces intermediate state-change records during a seek interaction. A final transition to `ended` or `stopped` normally precedes `view_ended`.
<expandable title="playback_state_changed payload">
```json
{
"schema_version": 1,
"event_name": "chunkify.video.playback_state_changed",
"event_id": "1e89e533-f58f-5194-8cba-97f3fa3bf9ca",
"sequence": 2,
"occurred_at": "2026-10-22T12:00:00.500Z",
"view_id": "06883d69-3643-40bf-b6a0-9556674fdccf",
"video_id": "ast_3Ljtoej7c3Y1plabZoElbQwdwLh",
"sdk_name": "@chunkify/player-analytics",
"sdk_version": "0.11.0",
"connector_name": "hls.js",
"connector_version": "1.6.5",
"player_name": "hls.js",
"player_version": "1.6.5",
"visibility_state": "visible",
"delivery_hostname": "media.example.com",
"environment": "production",
"viewer_id": "3f0c6a1e-2b7d-4c59-9e1a-7d4b2f8c6e10",
"user_id": null,
"video_title": null,
"reason": null,
"startup_outcome": null,
"error_category": null,
"error_code": null,
"error_fatal": null,
"error_playhead_seconds": null,
"playhead_seconds": 0,
"from_state": "startup",
"to_state": "playing",
"previous_state_duration_ms": 500
}
```
</expandable>
### Rendition changed
`rendition_changed` reports the initial known rendition and later changes. The initial selection has `null` previous-rendition fields. Repeated reports of the same rendition do not emit another record.
The connector must expose rendition information. Initial selection is not counted as a rendition switch.
| Fields | Meaning |
| --- | --- |
| `playhead_seconds` | Media position at the change, or `null`. |
| `player_width_px`, `player_height_px` | Displayed media size in CSS pixels. |
| `previous_rendition_*` | Previous rendition's `width`, `height`, `codec`, `bitrate_bps`, and `frame_rate`. |
| `selected_rendition_*` | Selected rendition's `width`, `height`, `codec`, `bitrate_bps`, and `frame_rate`. |
Rendition dimensions are pixels, bitrate is bits per second, and frame rate is frames per second. Individual properties can be `null`.
<expandable title="rendition_changed payload">
```json
{
"schema_version": 1,
"event_name": "chunkify.video.rendition_changed",
"event_id": "20b097dc-4a69-5e40-a1b6-f899c3d7807a",
"sequence": 4,
"occurred_at": "2026-10-22T12:00:10.500Z",
"view_id": "06883d69-3643-40bf-b6a0-9556674fdccf",
"video_id": "ast_3Ljtoej7c3Y1plabZoElbQwdwLh",
"sdk_name": "@chunkify/player-analytics",
"sdk_version": "0.11.0",
"connector_name": "hls.js",
"connector_version": "1.6.5",
"player_name": "hls.js",
"player_version": "1.6.5",
"visibility_state": "visible",
"delivery_hostname": "media.example.com",
"environment": "production",
"viewer_id": "3f0c6a1e-2b7d-4c59-9e1a-7d4b2f8c6e10",
"user_id": null,
"video_title": null,
"reason": null,
"startup_outcome": null,
"error_category": null,
"error_code": null,
"error_fatal": null,
"error_playhead_seconds": null,
"playhead_seconds": 10,
"player_width_px": 640,
"player_height_px": 360,
"previous_rendition_width": 1280,
"previous_rendition_height": 720,
"previous_rendition_codec": "avc1.64001f",
"previous_rendition_bitrate_bps": 1500000,
"previous_rendition_frame_rate": 30,
"selected_rendition_width": 1920,
"selected_rendition_height": 1080,
"selected_rendition_codec": "avc1.640028",
"selected_rendition_bitrate_bps": 3000000,
"selected_rendition_frame_rate": 30
}
```
</expandable>
### Seek completed
`seek_completed` summarizes a logical seek interaction. Rapid seeks are grouped until 400 ms have passed since the last `seeked`. If playback was running, the tracker also waits for it to resume.
| Fields | Meaning |
| --- | --- |
| `seek_started_at` | UTC timestamp of the first seek. |
| `last_seeked_at` | UTC timestamp of the final `seeked`, or `null`. |
| `playback_resumed_at` | UTC timestamp when playback resumes, or `null`. |
| `state_before_seek`, `state_after_seek` | Playback states surrounding the interaction. |
| `from_playhead_seconds`, `to_playhead_seconds` | Starting and final media positions, or `null` when unavailable. |
| `seek_interaction_duration_ms` | Total time spent seeking during this interaction. |
| `seek_resume_wait_ms` | Time from the final `seeked` to resumed playback, or `null`. |
| `seek_outcome` | `resumed`, `paused`, or `interrupted`. |
A view ending, a source change, or a fatal error can flush a pending seek as `interrupted`. Use the seek timestamps to locate the interaction; `occurred_at` is when its summary is emitted.
<expandable title="seek_completed payload">
```json
{
"schema_version": 1,
"event_name": "chunkify.video.seek_completed",
"event_id": "f466b2ec-b40d-5a81-861c-bc136602c0bf",
"sequence": 5,
"occurred_at": "2026-10-22T12:00:20.800Z",
"view_id": "06883d69-3643-40bf-b6a0-9556674fdccf",
"video_id": "ast_3Ljtoej7c3Y1plabZoElbQwdwLh",
"sdk_name": "@chunkify/player-analytics",
"sdk_version": "0.11.0",
"connector_name": "hls.js",
"connector_version": "1.6.5",
"player_name": "hls.js",
"player_version": "1.6.5",
"visibility_state": "visible",
"delivery_hostname": "media.example.com",
"environment": "production",
"viewer_id": "3f0c6a1e-2b7d-4c59-9e1a-7d4b2f8c6e10",
"user_id": null,
"video_title": null,
"reason": null,
"startup_outcome": null,
"error_category": null,
"error_code": null,
"error_fatal": null,
"error_playhead_seconds": null,
"seek_started_at": "2026-10-22T12:00:20.000Z",
"last_seeked_at": "2026-10-22T12:00:20.400Z",
"playback_resumed_at": "2026-10-22T12:00:20.520Z",
"state_before_seek": "playing",
"state_after_seek": "playing",
"from_playhead_seconds": 20,
"to_playhead_seconds": 45,
"seek_interaction_duration_ms": 400,
"seek_resume_wait_ms": 120,
"seek_outcome": "resumed"
}
```
</expandable>
### Subtitle changed
`subtitle_changed` reports a change of the showing subtitle or caption track during a view. The tracker reads the media element's text tracks, so it detects the language choices made in Chunkify Player's menus as well as tracks your code turns on or off.
| Field | Meaning |
| --- | --- |
| `previous_subtitle_language` | Language before the change, or `null` when no subtitles were detected. |
| `subtitle_language` | Language after the change, or `null`. See [Subtitles and playback speed](#subtitles-and-playback-speed). |
| `playhead_seconds` | Media position when the change was recorded, or `null`. |
The record is sent only when the language changes. The language when a view starts is in `view_started`.
<expandable title="subtitle_changed payload">
```json
{
"schema_version": 1,
"event_name": "chunkify.video.subtitle_changed",
"event_id": "8c2f6e1d-5b7a-4f3e-9a10-2d4c6b8e0f12",
"sequence": 3,
"occurred_at": "2026-10-22T12:00:10.500Z",
"view_id": "06883d69-3643-40bf-b6a0-9556674fdccf",
"video_id": "ast_3Ljtoej7c3Y1plabZoElbQwdwLh",
"sdk_name": "@chunkify/player-analytics",
"sdk_version": "0.11.0",
"connector_name": "hls.js",
"connector_version": "1.6.5",
"player_name": "hls.js",
"player_version": "1.6.5",
"visibility_state": "visible",
"delivery_hostname": "media.example.com",
"environment": "production",
"viewer_id": "3f0c6a1e-2b7d-4c59-9e1a-7d4b2f8c6e10",
"user_id": null,
"video_title": null,
"reason": null,
"startup_outcome": null,
"error_category": null,
"error_code": null,
"error_fatal": null,
"error_playhead_seconds": null,
"playhead_seconds": 10,
"previous_subtitle_language": null,
"subtitle_language": "fr"
}
```
</expandable>
### Playback rate changed
`playback_rate_changed` reports a change of playback speed during a view, such as a choice in Chunkify Player's speed menu.
| Field | Meaning |
| --- | --- |
| `previous_playback_rate` | Speed before the change, or `null`. |
| `playback_rate` | Speed after the change, or `null`. See [Subtitles and playback speed](#subtitles-and-playback-speed). |
| `playhead_seconds` | Media position when the change was recorded, or `null`. |
The record is sent only when the speed changes after rounding. With Shaka Player, the tracker reports Shaka's playback rate, so buffering is not recorded as a speed change. The speed when a view starts is in `view_started`.
<expandable title="playback_rate_changed payload">
```json
{
"schema_version": 1,
"event_name": "chunkify.video.playback_rate_changed",
"event_id": "4a9d1c7e-2f6b-4e8a-b3c5-7d1e9f0a2b46",
"sequence": 7,
"occurred_at": "2026-10-22T12:00:51.000Z",
"view_id": "06883d69-3643-40bf-b6a0-9556674fdccf",
"video_id": "ast_3Ljtoej7c3Y1plabZoElbQwdwLh",
"sdk_name": "@chunkify/player-analytics",
"sdk_version": "0.11.0",
"connector_name": "hls.js",
"connector_version": "1.6.5",
"player_name": "hls.js",
"player_version": "1.6.5",
"visibility_state": "visible",
"delivery_hostname": "media.example.com",
"environment": "production",
"viewer_id": "3f0c6a1e-2b7d-4c59-9e1a-7d4b2f8c6e10",
"user_id": null,
"video_title": null,
"reason": null,
"startup_outcome": null,
"error_category": null,
"error_code": null,
"error_fatal": null,
"error_playhead_seconds": null,
"playhead_seconds": 75,
"previous_playback_rate": 1,
"playback_rate": 1.5
}
```
</expandable>
### Playback error
`playback_error` includes cumulative view metrics and the first reported error in that view. Later error reports in the same view do not produce additional error records.
| Field | Meaning |
| --- | --- |
| `error_category` | `aborted`, `network`, `decode`, `unsupported`, or `unknown`. |
| `error_code` | Browser or engine code, as a string or number, or `null`. |
| `error_fatal` | Whether the reported error is fatal. |
| `error_playhead_seconds` | Media position when the error was recorded, or `null`. |
Raw error messages and URLs are omitted. A fatal error moves the view into the `error` state; `view_ended` still reports how the attempt later closes.
<expandable title="playback_error payload">
```json
{
"schema_version": 1,
"event_name": "chunkify.video.playback_error",
"event_id": "bd56d388-c447-579b-b699-dbc352f05bd9",
"sequence": 8,
"occurred_at": "2026-10-22T12:01:15.000Z",
"view_id": "06883d69-3643-40bf-b6a0-9556674fdccf",
"video_id": "ast_3Ljtoej7c3Y1plabZoElbQwdwLh",
"sdk_name": "@chunkify/player-analytics",
"sdk_version": "0.11.0",
"connector_name": "hls.js",
"connector_version": "1.6.5",
"player_name": "hls.js",
"player_version": "1.6.5",
"visibility_state": "visible",
"delivery_hostname": "media.example.com",
"environment": "production",
"viewer_id": "3f0c6a1e-2b7d-4c59-9e1a-7d4b2f8c6e10",
"user_id": null,
"video_title": null,
"reason": null,
"startup_outcome": null,
"error_category": "network",
"error_code": "fragLoadError",
"error_fatal": true,
"error_playhead_seconds": 95,
"media_duration_seconds": 120,
"playhead_seconds": 95,
"played_ranges": [
{
"start_seconds": 0,
"end_seconds": 10
},
{
"start_seconds": 35,
"end_seconds": 95
}
],
"startup_time_ms": 500,
"first_video_frame_time_ms": 520,
"playing_time_ms": 70000,
"rebuffer_count": 1,
"rebuffer_duration_ms": 2000,
"rebuffer_ratio": 0.027777777777777776,
"seek_count": 1,
"seek_duration_ms": 400,
"maximum_playhead_seconds": 95,
"furthest_progress_ratio": 0.7916666666666666,
"media_width_px": 1920,
"media_height_px": 1080,
"player_width_px": 640,
"player_height_px": 360,
"dropped_video_frames": 0,
"total_video_frames": 2100,
"selected_rendition_width": 1920,
"selected_rendition_height": 1080,
"selected_rendition_codec": "avc1.640028",
"selected_rendition_bitrate_bps": 3000000,
"rendition_switch_count": 1,
"rendition_playing_time": [
{
"width": 1280,
"height": 720,
"codec": "avc1.64001f",
"bitrate_bps": 1500000,
"frame_rate": 30,
"playing_time_ms": 10000
},
{
"width": 1920,
"height": 1080,
"codec": "avc1.640028",
"bitrate_bps": 3000000,
"frame_rate": 30,
"playing_time_ms": 60000
}
],
"delivery_request_count": 40,
"delivery_transferred_bytes": 16000000,
"delivery_throughput_bps": 8000000,
"subtitle_language": "fr",
"playback_rate": 1.5
}
```
</expandable>
### View ended
`view_ended` closes an attempt and includes its final cumulative metrics. It can be sent when playback finishes, monitoring stops, the source changes, or the page exits.
| Field | Values |
| --- | --- |
| `reason` | `ended`, `destroyed`, `source_changed`, or `page_exit`. |
| `startup_outcome` | `started`, `failed_before_start`, or `ended_before_start`. |
Only `reason: "ended"` means playback reached the media end. `startup_outcome: "started"` means the tracker observed playback begin. An attempt can end before playback starts without a reported failure.
<expandable title="view_ended payload">
```json
{
"schema_version": 1,
"event_name": "chunkify.video.view_ended",
"event_id": "310dc9da-92ab-55b8-8cdb-22b4aa8da908",
"sequence": 10,
"occurred_at": "2026-10-22T12:01:16.000Z",
"view_id": "06883d69-3643-40bf-b6a0-9556674fdccf",
"video_id": "ast_3Ljtoej7c3Y1plabZoElbQwdwLh",
"sdk_name": "@chunkify/player-analytics",
"sdk_version": "0.11.0",
"connector_name": "hls.js",
"connector_version": "1.6.5",
"player_name": "hls.js",
"player_version": "1.6.5",
"visibility_state": "visible",
"delivery_hostname": "media.example.com",
"environment": "production",
"viewer_id": "3f0c6a1e-2b7d-4c59-9e1a-7d4b2f8c6e10",
"user_id": null,
"video_title": null,
"reason": "destroyed",
"startup_outcome": "started",
"error_category": null,
"error_code": null,
"error_fatal": null,
"error_playhead_seconds": null,
"media_duration_seconds": 120,
"playhead_seconds": 95,
"played_ranges": [
{
"start_seconds": 0,
"end_seconds": 10
},
{
"start_seconds": 35,
"end_seconds": 95
}
],
"startup_time_ms": 500,
"first_video_frame_time_ms": 520,
"playing_time_ms": 70000,
"rebuffer_count": 1,
"rebuffer_duration_ms": 2000,
"rebuffer_ratio": 0.027777777777777776,
"seek_count": 1,
"seek_duration_ms": 400,
"maximum_playhead_seconds": 95,
"furthest_progress_ratio": 0.7916666666666666,
"media_width_px": 1920,
"media_height_px": 1080,
"player_width_px": 640,
"player_height_px": 360,
"dropped_video_frames": 0,
"total_video_frames": 2100,
"selected_rendition_width": 1920,
"selected_rendition_height": 1080,
"selected_rendition_codec": "avc1.640028",
"selected_rendition_bitrate_bps": 3000000,
"rendition_switch_count": 1,
"rendition_playing_time": [
{
"width": 1280,
"height": 720,
"codec": "avc1.64001f",
"bitrate_bps": 1500000,
"frame_rate": 30,
"playing_time_ms": 10000
},
{
"width": 1920,
"height": 1080,
"codec": "avc1.640028",
"bitrate_bps": 3000000,
"frame_rate": 30,
"playing_time_ms": 60000
}
],
"delivery_request_count": 40,
"delivery_transferred_bytes": 16000000,
"delivery_throughput_bps": 8000000,
"subtitle_language": "fr",
"playback_rate": 1.5,
"subtitle_playing_time": [
{
"language": null,
"playing_time_ms": 10000
},
{
"language": "fr",
"playing_time_ms": 60000
}
],
"playback_rate_playing_time": [
{
"rate": 1,
"playing_time_ms": 50000
},
{
"rate": 1.5,
"playing_time_ms": 20000
}
]
}
```
</expandable>
## View metrics
`view_started`, `view_snapshot`, `playback_error`, and `view_ended` include the metrics below. Time counters accumulate within a view; dimensions and rendition fields describe the current playback state.
State, rendition, seek, subtitle, and playback-rate records contain their event-specific fields instead.
### Time and progress
| Field | Meaning |
| --- | --- |
| `playhead_seconds` | Current media position, or `null`. |
| `played_ranges` | Merged media sections played by this attempt, or `null` when unavailable. |
| `media_duration_seconds` | Media duration, or `null` when unknown. |
| `startup_time_ms` | Time from the attempt starting to the first `playing` event. |
| `first_video_frame_time_ms` | Time to the first video frame callback when the browser exposes it. It can differ from startup time. |
| `playing_time_ms` | Wall-clock time spent playing, excluding pauses, buffering, and seeking. |
| `rebuffer_count` | Buffering interruptions after startup, excluding seeks. |
| `rebuffer_duration_ms` | Cumulative time spent buffering after startup. |
| `rebuffer_ratio` | Buffering time divided by playing plus buffering time. `null` until that total is positive. |
| `seek_count` | Number of grouped seek interactions. |
| `seek_duration_ms` | Cumulative time spent seeking. |
| `maximum_playhead_seconds` | Furthest media position reached. |
| `furthest_progress_ratio` | Furthest position divided by media duration, capped at `1`. Seeking forward increases it without watching the intervening video. |
### Video and player
| Fields | Meaning |
| --- | --- |
| `media_width_px`, `media_height_px` | The media element's intrinsic video dimensions. They can differ from encoded rendition dimensions. |
| `player_width_px`, `player_height_px` | Displayed media box in CSS pixels. For Chunkify Player, this is the inner media element. |
| `dropped_video_frames`, `total_video_frames` | Per-view browser frame counters, when available. |
Player dimensions are sampled when records are emitted. The final event retains the last measurable size if the element has already been removed.
### Renditions and delivery
| Fields | Meaning |
| --- | --- |
| `selected_rendition_width`, `selected_rendition_height` | Current rendition dimensions in pixels. |
| `selected_rendition_codec` | Current video codec string. |
| `selected_rendition_bitrate_bps` | Current rendition bitrate in bits per second. |
| `rendition_switch_count` | Changes between known renditions during the view. |
| `rendition_playing_time` | Cumulative playing time per known rendition. Each entry contains `width`, `height`, `codec`, `bitrate_bps`, `frame_rate`, and `playing_time_ms`. |
| `delivery_request_count` | Completed media requests observed by the connector. |
| `delivery_transferred_bytes` | Response bytes observed by the connector. |
| `delivery_throughput_bps` | Aggregate transfer throughput in bits per second. |
`rendition_playing_time` is `null` without a rendition-aware connector and `[]` before one identifies a rendition. It excludes playing time before the rendition is known, so its total can be lower than `playing_time_ms`.
Engine metrics depend on the active connector. hls.js, Shaka, and Video.js 10 with its hls.js engine expose rendition and request data when available.
Native `<video>` and Chunkify Player's native HLS or MP4 playback leave engine metrics `null`.
### Subtitles and playback speed
| Fields | Meaning |
| --- | --- |
| `subtitle_language` | Language of the showing subtitle or caption track, such as `fr` or `en-US`. `und` means a track without a declared language. `null` means no subtitles were detected. |
| `playback_rate` | Current playback speed, rounded to two decimals, such as `1` or `1.5`. `null` for a speed outside the reported range, such as `0`. |
| `subtitle_playing_time` | Cumulative playing time per subtitle language, including `null` for time without subtitles. Each entry contains `language` and `playing_time_ms`. Only on `view_snapshot` and `view_ended`. |
| `playback_rate_playing_time` | Cumulative playing time per speed. Each entry contains `rate` and `playing_time_ms`. Only on `view_snapshot` and `view_ended`. |
`null` subtitles means either subtitles are off or they cannot be detected. The tracker reads the media element's showing `subtitles` or `captions` track, so subtitles that a player draws without a showing text track, such as Shaka's built-in subtitle display, are reported as `null`. When several tracks show at once, the first one is reported.
The playing-time lists count only time spent playing, like `rendition_playing_time`. Each list keeps at most 32 distinct values; playing time in further values is not added, so a list's total can be lower than `playing_time_ms`. Playing time at a `null` speed is not attributed to a speed.
## Watched sections and background playback
`played_ranges` records the sections actually played as cumulative `start_seconds` and `end_seconds` pairs. Overlaps and replays merge within an attempt; seeking over a section does not mark it watched.
An empty array means no measured section. `null` means coverage is unavailable, including after the tracker exceeds 128 disjoint ranges. Use the final record, or the latest snapshot, for [watched-section reports](/docs/integration/analytics/server/dashboard#engagement).
`visibility_state` records the document visibility at event time. A pause recorded while hidden can be shown as "Paused in background", but it does not establish whether the browser, application, or viewer caused it.
## Use events in reports
Count distinct `view_id` values from `view_started` to count attempts. To count confirmed playback starts, use the first transition to `playing`, or final records with `startup_outcome: "started"`.
**Do not add cumulative snapshots together.** Use each view's final totals, or differences between successive snapshots. A final record can be lost when the page closes, so snapshots can provide an earlier result.
Deduplicate by `event_id` and order a view's records by `sequence`. Distinct `viewer_id` values approximate unique viewers per browser; each `view_id` identifies an attempt.
See [Send events](/docs/integration/analytics/send-events) to choose destinations and transform the payload they receive.