npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@clappr/telemetry

v0.2.8

Published

Telemetry package for Clappr player

Readme

@clappr/telemetry

npm version

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.sample event 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/telemetry

Via 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_TRACE

This 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:watch

The 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