# 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.
