@clappr/telemetry
v0.2.8
Published
Telemetry package for Clappr player
Maintainers
Keywords
Readme
@clappr/telemetry
A telemetry plugin for Clappr. Collects network, buffer, decoding, playback state, playback timing, and stream info metrics from the player and exposes them through a single unified event on the container bus.
Overview
@clappr/telemetry is a ContainerPlugin that automatically detects the active playback engine and activates the appropriate adapter for metrics collection. All telemetry data — regardless of source or event type — flows through the same registered event channel using a versioned envelope format, keeping consumers decoupled from internal implementation details.
The plugin is built around three extensible subsystems:
- Network adapters — hook into the streaming engine (Shaka, HLS.js) to capture request timings, bitrate changes, and DRM events
- Sampler registry — drives a set of periodic samplers from a single timer, emitting one
mse.sampleevent per tick with data grouped by key (buffer,decoding,playbackState) - Observer registry — manages active observers and fires lifecycle calls on each; extensible via
ObserverRegistry.register()so custom observers can be added without forking the package
Installation
yarn add @clappr/telemetryVia CDN (jsDelivr):
<script src="https://cdn.jsdelivr.net/npm/@clappr/telemetry/dist/clappr-telemetry.js"></script>Usage
<script src="https://cdn.jsdelivr.net/npm/@clappr/core/dist/clappr-core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dash-shaka-playback/dist/dash-shaka-playback.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@clappr/telemetry/dist/clappr-telemetry.js"></script>
<div id="player"></div>
<script>
const {
ShakaNetworkAdapter, HlsNetworkAdapter,
BufferSampler, DecodingSampler, PlaybackStateSampler,
NetworkSampler, PlaybackTimingSampler, StreamInfoSampler,
VideoEventObserver
} = ClapprTelemetry
const player = new Clappr.Player({
parentId: '#player',
source: 'https://example.com/stream.mpd',
plugins: [DashShakaPlayback, ClapprTelemetry],
telemetry: {
adapters: [ShakaNetworkAdapter, HlsNetworkAdapter],
samplers: [BufferSampler, DecodingSampler, PlaybackStateSampler,
NetworkSampler, PlaybackTimingSampler, StreamInfoSampler],
observers: [VideoEventObserver],
sampleIntervalMs: 1000,
}
})
</script>With npm/ESM:
import Clappr from '@clappr/core'
import DashShakaPlayback from 'dash-shaka-playback'
import ClapprTelemetry, {
ShakaNetworkAdapter, HlsNetworkAdapter,
BufferSampler, DecodingSampler, PlaybackStateSampler,
NetworkSampler, PlaybackTimingSampler, StreamInfoSampler,
VideoEventObserver
} from '@clappr/telemetry'
const player = new Clappr.Player({
parentId: '#player',
source: 'https://example.com/stream.mpd',
plugins: [DashShakaPlayback, ClapprTelemetry],
telemetry: {
adapters: [ShakaNetworkAdapter, HlsNetworkAdapter],
samplers: [BufferSampler, DecodingSampler, PlaybackStateSampler,
NetworkSampler, PlaybackTimingSampler, StreamInfoSampler],
observers: [VideoEventObserver],
sampleIntervalMs: 1000,
}
})Named exports (ES modules)
Available from dist/clappr-telemetry.esm.js:
Adapters
| Export | Description |
| --------------------- | ------------------------------------------------------------------------------ |
| NetworkAdapters | Registry class — use to register and unregister adapters (none pre-registered) |
| ShakaNetworkAdapter | Shaka network metrics adapter class — must be registered explicitly |
| HlsNetworkAdapter | HLS.js network metrics adapter class — must be registered explicitly |
Samplers
| Export | Description |
| ----------------------- | ---------------------------------------------------------------- |
| SamplerRegistry | Registry class — use to register custom samplers |
| BufferSampler | Built-in buffer state sampler |
| DecodingSampler | Built-in decoding quality sampler |
| PlaybackStateSampler | Built-in sampler for networkState, paused, playbackRate, and bitrate |
| NetworkSampler | Built-in sampler for request counters, throughput, and segment metrics |
| PlaybackTimingSampler | Built-in sampler for timePlayingMs, timeWaitingMs, and joinTimeMs |
| StreamInfoSampler | Built-in sampler for stream metadata (container format, codecs, variant count) |
Observers
| Export | Description |
| -------------------- | --------------------------------------------------------------- |
| ObserverRegistry | Registry class — use to register custom observers |
| VideoEventObserver | Built-in observer for native <video> DOM events |
Utils
| Export | Description |
| ---------------------------- | ---------------------------------------------------------------- |
| TELEMETRY_CONTRACT_VERSION | Semver string on each envelope (v field) |
| EVENT_TYPES | Canonical type strings (request:start, mse.sample, etc.) |
| TELEMETRY_SOURCES | Canonical source values (e.g. network) |
| createEnvelope | Builds the versioned envelope object |
| emitTelemetry | Triggers Events.Custom.CONTAINER_TELEMETRY_TRACE on an emitter |
| calculateThroughput | Mbps helper used by network adapters |
| getBufferAhead | Returns seconds buffered ahead of the current position |
| getBufferedRanges | Converts TimeRanges to a compact [[start, end], ...] array |
| DEFAULT_VIDEO_EVENTS | Array with the 14 default HTMLVideoElement event names observed |
The UMD build (dist/clappr-telemetry.js / CDN) exposes only the plugin as the global ClapprTelemetry. Adapters must be registered explicitly via ClapprTelemetry.NetworkAdapters — no adapter is pre-registered in any build.
Configuration
All options are opt-in — nothing is collected by default. Components are activated by including their class in the corresponding array.
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| telemetry.enabled | Boolean | true | Set to false to disable the plugin entirely for this player instance |
| telemetry.adapters | Class[] | [] | Network adapter classes to activate for this player instance. The first one in the array whose isSupported(playback) returns true is used |
| telemetry.samplers | Class[] | [] | Sampler classes to register for this player instance |
| telemetry.observers | Class[] | [] | Observer classes to register for this player instance |
| telemetry.sampleIntervalMs | Number | 0 | Sampling interval in ms. When 0 (default), no automatic interval is started — use snapshot() for on-demand collection |
| telemetry.<name>.enabled | Boolean | true | Opt-out flag for any registered component. Key is the component's static get name() value (e.g. buffer, decoding, videoState) |
| telemetry.bufferSample.includeRanges | Boolean | true | Include buffered time ranges in the mse.sample payload |
| telemetry.videoState.videoEvents | String[] | 14 events | HTMLVideoElement event names for VideoEventObserver to observe. Defaults to the full list (see VideoEventObserver) |
Disabling individual components
Each instance only activates its own samplers/observers/adapters arrays. When sharing a config object across players, use <name>.enabled: false to disable a component for one instance without touching the shared array:
new Clappr.Player({
telemetry: {
samplers: [BufferSampler, DecodingSampler, NetworkSampler, ...],
observers: [VideoEventObserver],
// disable specific components by their static get name() value:
decoding: { enabled: false },
videoState: { enabled: false },
}
})The <name> key maps to each class's static get name() property:
| Class | Key |
| --- | --- |
| BufferSampler | buffer |
| DecodingSampler | decoding |
| PlaybackStateSampler | playbackState |
| NetworkSampler | network |
| PlaybackTimingSampler | timing |
| StreamInfoSampler | streamInfo |
| VideoEventObserver | videoState |
Consuming telemetry events
All telemetry data is emitted on a single event registered by the plugin: Clappr.Events.Custom.CONTAINER_TELEMETRY_TRACE. The recommended way to consume it is via a ContainerPlugin, which handles lifecycle and cleanup automatically:
class MyTelemetryConsumer extends Clappr.ContainerPlugin {
get name() { return 'my_telemetry_consumer' }
get supportedVersion() { return { min: '0.13.1' } }
bindEvents() {
this.listenTo(this.container, Clappr.Events.Custom.CONTAINER_TELEMETRY_TRACE, this.onTrace)
}
onTrace(envelope) {
if (envelope.type === 'mse.sample') {
const { buffer, decoding, playbackState } = envelope.data
}
if (envelope.type === 'media.event') {
const { name, currentTime, readyState, snapshot } = envelope.data
}
}
}Event channel
All telemetry data flows through a single registered event on the container bus:
Clappr.Events.Custom.CONTAINER_TELEMETRY_TRACEThis event is registered when the TelemetryPlugin is instantiated. It is the public contract of the plugin. Consumers never need to know which adapter or engine is active — they always listen to the same event and receive the same envelope shape.
Envelope format
Every emission on CONTAINER_TELEMETRY_TRACE carries a versioned envelope:
| Field | Type | Description |
| -------- | ------ | ----------------------------------------------------------- |
| type | string | What happened — identifies the event within its source area |
| source | string | Where it came from — which telemetry area emitted the event |
| data | object | Event-specific payload (varies by type) |
| t | number | Monotonic timestamp from performance.now() |
| ts | number | Wall-clock timestamp from Date.now() |
| v | string | Envelope contract version (1.0) |
Adapters
Adapters connect the plugin to specific playback engines. Each adapter implements static isSupported(playback) and bind().
| Adapter | Engine | Status |
| --------------------- | --------------------- | --------- |
| ShakaNetworkAdapter | dash-shaka-playback | Available |
| HlsNetworkAdapter | hlsjs-playback | Available |
Registering adapters
All adapters — including the built-ins — must be registered before the player is instantiated. The first registered adapter has the highest priority.
import { NetworkAdapters, ShakaNetworkAdapter, HlsNetworkAdapter } from '@clappr/telemetry'
NetworkAdapters.register(ShakaNetworkAdapter)
NetworkAdapters.register(HlsNetworkAdapter)
// Custom adapters can also be registered
NetworkAdapters.register(MyCustomAdapter)
// Remove when no longer needed
NetworkAdapters.unregister(MyCustomAdapter)Adapter contract:
| Member | Description |
| --- | --- |
| static get name() | String identifier (optional — used only in log messages) |
| static isSupported(playback) | Returns true when this adapter handles the given playback |
| constructor(playback, container) | Receives playback engine and container |
| bind() | Attaches listeners/hooks into the engine |
| destroy() | Detaches listeners and releases resources |
Sources (source)
| source | Area |
| ---------------------- | ----------------------------------------------------------------- |
| network | Network request metrics (segments, manifests, licenses) |
| sampler-registry | Periodic MSE metrics (buffer, decoding, playback state) |
| video-event-observer | Native <video> DOM events |
Event types (type)
| type | source | Description |
| ------------------------ | ---------------------- | ----------------------------------------------------- |
| request:start | network | A network request was initiated |
| request:end | network | A network request completed |
| request:error | network | A network request failed |
| bitrate:init | network | Initial quality variant is known (first segment loaded) |
| bitrate:change | network | ABR algorithm switched to a different quality variant |
| stream:info | network | Stream metadata available (manifest loaded or variant changed) |
| drm:session:update | network | A DRM session was updated |
| drm:expiration:updated | network | A DRM license expiration time was updated |
| mse.sample | sampler-registry | Periodic snapshot of buffer, decoding and/or playback state |
| media.event | video-event-observer | A native HTMLVideoElement DOM event fired |
mse.sample payload
Emitted once per tick by the SamplerRegistry. Each key is only present when the respective sampler is enabled and has data to report (the decoding key is absent on the first tick while the baseline is being established).
{
buffer: {
bufferAhead: 20, // seconds buffered ahead of current position
currentTime: 10, // current playback position in seconds
rangesCompact: [[0, 30]] // buffered time ranges (omitted if includeRanges: false)
},
decoding: {
decodedFps: 24, // frames decoded per second in the interval
droppedFps: 1, // frames dropped per second in the interval
dropRatio: 0.04, // ratio of dropped frames over total in the interval
currentTime: 10, // current playback position in seconds
totalDropped: 2, // cumulative dropped frames since playback started
totalDecoded: 537 // cumulative decoded frames since playback started
},
playbackState: {
networkState: 2, // HTMLVideoElement.networkState (0–3)
paused: false, // whether the player is paused
playbackRate: 1, // current playback rate
currentTime: 10, // current playback position in seconds
bitrateKbps: 4500, // current ABR variant bitrate in kbps (null before first segment)
width: 1920, // current variant horizontal resolution (null before first segment)
height: 1080, // current variant vertical resolution (null before first segment)
switchesUp: 2, // cumulative ABR upgrades since playback started
switchesDown: 0 // cumulative ABR downgrades since playback started
},
network: {
throughputEwmaMbps: 18.4, // exponentially weighted moving average throughput in Mbps
lastThroughputMbps: 20.1, // throughput of the last completed segment request
activeRequests: 1, // number of in-flight requests
segmentsLoaded: 42, // cumulative segments successfully loaded
segmentErrors: 0, // cumulative segment request failures
totalBytesKB: 8320, // cumulative bytes downloaded (segments only)
avgSegmentLoadTimeMs: 210, // average segment load time in ms (null before first segment)
licenseRequests: 1, // cumulative DRM license requests
licenseErrors: 0, // cumulative DRM license failures
fatalErrors: 0, // cumulative fatal adapter errors
drmExpirationTime: null, // DRM license expiration timestamp (ms), null if not set
networkQuality: { label: 'good', score: 0.9 }, // throughput tier classification
networkAdequacy: { label: 'adequate', score: 0.8 }, // throughput adequacy vs current bitrate
segmentHistory: [...] // recent segment load records
},
timing: {
timePlayingMs: 120000, // cumulative ms in playing state
timeWaitingMs: 3200, // cumulative ms in waiting/buffering state
joinTimeMs: 850, // ms from play() to first playing event (null before first play)
autoplayStartupTimeMs: 1200, // ms to first frame when no play() call was made (null on manual play)
manifestLoadTimeMs: 320, // first manifest load time in ms (null until received via network trace)
firstSegmentLoadTimeMs: 180 // first segment load time in ms (null until received via network trace)
},
streamInfo: {
container: 'MP4', // container format derived from the video MIME type
videoCodec: 'avc1', // parsed video codec string
audioCodec: 'mp4a', // parsed audio codec string
levelsCount: 8 // total number of variant tracks
}
}VideoEventObserver
The VideoEventObserver watches the native HTMLVideoElement and is engine-agnostic — it works identically with Shaka, HLS.js, or any other playback engine.
It emits media.event whenever one of the observed DOM events fires on the <video> element:
{
name: 'waiting', // the DOM event name
currentTime: 10.4, // position at the moment of the event
readyState: 2, // HTMLVideoElement.readyState
snapshot: { // current sampler data at the moment of the event
buffer: { bufferAhead: 0.2, currentTime: 10.4 },
decoding: { decodedFps: 24, droppedFps: 0, ... },
playbackState: { networkState: 2, paused: false, playbackRate: 1, currentTime: 10.4 }
}
}snapshot reflects the last sampler tick. Keys are only present when the respective sampler is enabled. If no sampler is active, snapshot is {}.
Default observed events: waiting, playing, stalled, seeking, seeked, ended, canplay, canplaythrough, loadedmetadata, loadeddata, error, emptied, suspend, abort.
Configuration
Include VideoEventObserver in the observers array to activate it. Use videoState.videoEvents to narrow the observed event list:
new Clappr.Player({
plugins: [ClapprTelemetry],
telemetry: {
observers: [VideoEventObserver],
videoState: {
videoEvents: ['waiting', 'stalled', 'error'] // default: full list above
}
}
})Custom samplers
The sampler registry is extensible. You can add your own sampler and have it participate in the same mse.sample tick without forking the package.
Contract
| Member | Required | Description |
| --- | --- | --- |
| static get name() | Yes | Unique string key — used as the registry identifier and as the key in the mse.sample payload |
| collect() | Yes | Called every tick. Returns a plain object with the data to include, or null to skip this tick |
| destroy() | Yes | Called when the registry is destroyed. Clean up any internal state here |
| static isEnabled(cfg) | No | Override the registry's default enable/disable check. cfg is container.options.telemetry. Omit to use the standard cfg[name].enabled opt-out |
Example
import { SamplerRegistry } from '@clappr/telemetry'
class AudioSampler {
static get name() { return 'audio' }
constructor(playback) {
this._playback = playback
}
collect() {
const videoEl = this._playback?.el
if (!videoEl) return null
return { volume: videoEl.volume, muted: videoEl.muted }
}
destroy() {
this._playback = null
}
}
// register before instantiating the player
SamplerRegistry.register(AudioSampler)
new Clappr.Player({
plugins: [ClapprTelemetry],
telemetry: {
samplers: [AudioSampler],
sampleIntervalMs: 1000,
}
})The result appears under the audio key in the mse.sample payload:
{
buffer: { ... },
decoding: { ... },
playbackState: { ... },
audio: { volume: 1, muted: false }
}To remove a previously registered sampler:
SamplerRegistry.unregister(AudioSampler)On-demand snapshot
The TelemetryPlugin exposes a snapshot getter that returns the current sampler data immediately, without waiting for the next tick and without emitting any event:
const telemetry = player.getPlugin('telemetry')
const data = telemetry.snapshot
// { buffer: { bufferAhead: 20, currentTime: 10 }, decoding: { ... }, playbackState: { ... } }Returns an empty object if called before the player is ready.
Custom observers
The observer registry is extensible. You can add your own observer and have it managed alongside the built-in ones.
Contract
| Member | Required | Description |
| --- | --- | --- |
| static get name() | Yes | Unique string key — used as the registry identifier |
| bind() | Yes | Called after instantiation. Attach event listeners and start collecting |
| destroy() | Yes | Called when the registry is destroyed. Remove listeners and clean up state |
| static isEnabled(cfg) | No | Override the registry's default enable/disable check. Omit to use the standard cfg[name].enabled opt-out |
The constructor receives (playback, container, samplerRegistry) — the same arguments as VideoEventObserver.
Example
import { ObserverRegistry } from '@clappr/telemetry'
class PlaybackEventObserver {
static get name() { return 'playbackEvents' }
constructor(playback, container, samplerRegistry) {
this._container = container
this._samplerRegistry = samplerRegistry
}
bind() {
this._container.on('playback:play', () => {
console.log('play', this._samplerRegistry?.snapshot())
})
}
destroy() {
this._container = null
this._samplerRegistry = null
}
}
// register before instantiating the player
ObserverRegistry.register(PlaybackEventObserver)
new Clappr.Player({
plugins: [ClapprTelemetry],
telemetry: {
observers: [PlaybackEventObserver],
}
})To remove a previously registered observer:
ObserverRegistry.unregister(PlaybackEventObserver)Development
This package lives in the Clappr monorepo under packages/clappr-telemetry. Install dependencies once at the repository root; build, dev server, and tests should be run with yarn workspace @clappr/telemetry so Yarn resolves the workspace correctly:
# From the monorepo root (not only inside this package)
yarn install
yarn workspace @clappr/telemetry build
yarn workspace @clappr/telemetry dev # Rollup watch + demo at http://localhost:8080
yarn workspace @clappr/telemetry test
yarn workspace @clappr/telemetry test:watchThe demo page expects UMD builds from sibling packages (for example @clappr/core and dash-shaka-playback). If those dist/ folders are missing, build them from the root (for example lerna run build --scope=@clappr/core and lerna run build --scope=dash-shaka-playback) before opening the dev server.
License
BSD-3-Clause
