Skip to content

Usage (Web)

The SDK can be used either with the generic ClientAnalytics class for plain HTML5 video, or with player-specific adapters that provide tighter integration with popular video players.

Warning

Set remoteEndpoint to the address of your analytics server, which you get from AVEQ. This is required in production. Without it, the SDK sends data to http://localhost:3004/client_analytics_api/v1, a local development address, and no data reaches your server.

Plain HTML5 Video

Create a ClientAnalytics instance with your API key and the address of your analytics server, then call measure() on the video element:

<script src="surfmeter-client-analytics.js"></script>
<script>
  const sca = new surfmeter.ClientAnalytics('YOUR_API_KEY', {
    remoteEndpoint: 'https://YOUR_SERVER/client_analytics_api/v1',
  });

  const video = document.querySelector('video');
  const handler = sca.measure(video, {
    streamId: 'my-stream-id',
    metadata: {
      userId: 'user123',
      channelId: 'channel456',
    },
  });
</script>

Video.js

Use the VideoJsClientAnalytics class. For HLS/DASH streaming with quality tracking, configure Video.js to use MSE rather than native playback:

<link href="https://vjs.zencdn.net/8.16.1/video-js.css" rel="stylesheet" />
<script src="https://vjs.zencdn.net/8.16.1/video.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/videojs-contrib-quality-levels@4/dist/videojs-contrib-quality-levels.min.js"></script>
<script src="surfmeter-videojs-analytics.js"></script>

<video-js id="my-player" class="vjs-default-skin" controls width="640" height="360"></video-js>

<script>
  var player = videojs('my-player', {
    html5: {
      vhs: { overrideNative: true },
      nativeAudioTracks: false,
      nativeVideoTracks: false,
    },
  });

  player.src({ src: 'https://example.com/stream.m3u8', type: 'application/x-mpegURL' });

  var analytics = new surfmeter.VideoJsClientAnalytics('YOUR_API_KEY', {

    remoteEndpoint: 'https://YOUR_SERVER/client_analytics_api/v1',

  });
  var handler = analytics.measure(player, { streamId: 'YOUR_STREAM_ID' });
</script>

The videojs-contrib-quality-levels plugin is recommended for tracking bitrate, resolution, and frame rate changes. Without it, the adapter still collects standard HTML video element metrics but cannot track player-level quality switches.

Shaka Player

Use the ShakaplayerAnalytics class. Call measure() after the player has loaded a source:

<script src="https://cdn.jsdelivr.net/npm/shaka-player/dist/shaka-player.compiled.js"></script>
<script src="surfmeter-shakaplayer-analytics.js"></script>

<script>
  const analytics = new surfmeter.ShakaplayerAnalytics('YOUR_API_KEY', {
    remoteEndpoint: 'https://YOUR_SERVER/client_analytics_api/v1',
  });
  const video = document.getElementById('video');
  const player = new shaka.Player(video);

  player.load('https://example.com/stream.mpd').then(() => {
    const handler = analytics.measure(player, { streamId: 'YOUR_STREAM_ID' });
  });
</script>

THEOplayer

Use the TheoplayerAnalytics class. Requires THEOplayer v8+:

<script src="https://cdn.jsdelivr.net/npm/theoplayer@10/THEOplayer.js"></script>
<script src="surfmeter-theoplayer-analytics.js"></script>

<script>
  const analytics = new surfmeter.TheoplayerAnalytics('YOUR_API_KEY', {
    remoteEndpoint: 'https://YOUR_SERVER/client_analytics_api/v1',
  });
  const player = new THEOplayer.Player(document.getElementById('player'), {
    libraryLocation: 'https://cdn.jsdelivr.net/npm/theoplayer@10/',
  });

  player.source = {
    sources: [{ src: 'https://example.com/stream.m3u8', type: 'application/x-mpegurl' }],
  };

  const handler = analytics.measure(player, { streamId: 'YOUR_STREAM_ID' });
</script>

Chromecast

For CAF (Cast Application Framework) Web Receivers, use the ChromecastAnalytics class. It measures the <video> element of the receiver, so call measure() once the media has loaded, for example on PLAYER_LOAD_COMPLETE. End the previous session when new media is loaded:

<script src="https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
<script src="surfmeter-chromecast-analytics-v2.0.1.bundle.js"></script>

<script>
  const analytics = new surfmeter.ChromecastAnalytics('YOUR_API_KEY', {
    remoteEndpoint: 'https://YOUR_SERVER/client_analytics_api/v1',
  });
  const context = cast.framework.CastReceiverContext.getInstance();
  const playerManager = context.getPlayerManager();
  let handler;

  playerManager.addEventListener(
    cast.framework.events.EventType.PLAYER_LOAD_COMPLETE,
    () => {
      handler?.destroy();
      handler = analytics.measure(playerManager, {
        streamId: 'YOUR_STREAM_ID',
      });
    }
  );

  context.start();
</script>

The SDK sends the URL of the loaded media (contentUrl, or contentId if there is no URL) as the manifest URL of the session, since the video element of the receiver usually plays a blob: URL.

Apps Built with a Bundler

The bundles are plain scripts that set a global surfmeter object. If your app is built with a bundler, add the script tag to your HTML page, before the player library, and use window.surfmeter in your app code:

const analytics = new window.surfmeter.ShakaplayerAnalytics('YOUR_API_KEY', {
  remoteEndpoint: 'https://YOUR_SERVER/client_analytics_api/v1',
});

API Reference

Constructor Options

All adapters accept the same constructor signature: new Analytics(apiKey, opts).

  • apiKey (string) — your Surfmeter API key.
  • opts (object):
    • remoteEndpoint (string) — the address of your analytics server, for example https://YOUR_SERVER/client_analytics_api/v1. Required in production. The default is the local development address http://localhost:3004/client_analytics_api/v1.
    • fingerprint (string | () => string | Promise\<string>) — a unique identifier for the device or session, used to correlate measurements across page reloads. When omitted, the library generates one automatically using FingerprintJS (open-source edition), with crypto.randomUUID() as a fallback.
    • deviceType ('pc' | 'mobile') — the type of device the video is watched on, used to calculate the video quality score (ITU-T P.1203). Use 'mobile' for smartphones and tablets. The device type changes the quality score, since the P.1203 model rates the same video differently on a small screen. Defaults to 'pc'. The SDK ignores other values.
    • displaySize ({ width: number, height: number }) — the display size in pixels as whole numbers greater than 0, used to calculate the video quality score. The SDK uses this value only if it cannot measure the size of the video element on the page, which it normally can. Defaults to 1920×1080. The SDK ignores invalid values.

For example, on a smartphone:

const analytics = new surfmeter.ShakaplayerAnalytics('YOUR_API_KEY', {
  remoteEndpoint: 'https://YOUR_SERVER/client_analytics_api/v1',
  deviceType: 'mobile',
  displaySize: { width: 390, height: 844 },
});

Measurement Options

analytics.measure(player, options?) starts monitoring. The options object supports:

  • streamId — a stable identifier for the content being played (video ID, channel ID, etc.). Recommended for all MSE-based players (Video.js, Shaka, THEOplayer), since the <video> element's src will be a blob: URL rather than the actual stream URL. If the SDK cannot determine the video source, it silently skips sending data.
  • metadata — arbitrary key-value object attached to the measurement. Useful for segmenting data in the dashboard (e.g., user ID, channel, content title).
  • params — additional parameters for the measurement.
  • contentType — the type of content: 'live', 'feature' (for example, a movie or an episode on demand), 'trailer', 'advertisement', 'lab' or 'unknown'. If you do not set it, the SDK sets 'live' if the video has no known duration, and 'unknown' otherwise. The SDK ignores other values, such as 'vod'. Use the same content type every time for the same streamId.
const handler = analytics.measure(player, { streamId: 'YOUR_STREAM_ID', contentType: 'feature' });

Handler

The object returned by measure() exposes:

  • surfmeterMonitoringId — unique identifier for this measurement session.
  • destroy() — end the session, send the final report, and remove all event listeners. If sending the final report fails, the SDK retries it. Call this when you tear down the player.
  • abort(reason?) — end the session as aborted, for example when the user leaves the video. reason is free text and defaults to aborted. Like destroy(), it sends the final report and removes all event listeners.
  • onPlaybackError(error?) — manually report an error from your application code (e.g., DRM failures, network errors caught outside the player). This ends the session as aborted with the reason playbackError. Accepts either a plain string or a structured object:

    // Simple string message
    handler.onPlaybackError('Some Error Message');
    
    // Structured error details
    handler.onPlaybackError({
      message: 'Failed to load segment',
      errorType: 'NETWORK',
      errorCode: '404',
    });
    
    - onAdBreakStarted() — signal that an ad break has started. - onAdBreakEnded() — signal that an ad break has ended. - onCdnChanged(hostname, ipAddress?) — signal that the video is now delivered from another CDN host.

Setting Metadata After Initialization

You can update metadata on a running measurement:

analytics.setMetadata(handler.surfmeterMonitoringId, {
  userId: 'user123',
  channelId: 'channel456',
});

Similarly, you can set additional parameters:

analytics.setMeasurementParams(handler.surfmeterMonitoringId, {
  customParam: 'value',
});

Warning

Please make sure that if you send personal data such as customer IDs, names, or email addresses, you have the necessary consent of the user to do so.

What It Monitors

All adapters collect the following metrics:

  • Playback state transitions (playing, paused, seeking, stalling, ended)
  • Buffering and stalling events
  • Video and audio codec changes
  • Bitrate changes (video and audio)
  • Resolution changes (width, height)
  • Frame rate changes
  • Playback errors (reported by the player for Video.js, Shaka Player, and THEOplayer, and with onPlaybackError() for all adapters)
  • Time updates and duration
  • Display size

Player-specific adapters additionally track quality information from the player's own API (e.g., quality levels in Video.js, track/variant data in Shaka Player, media track qualities in THEOplayer).

The bitrate is measured differently depending on the browser:

  • In Chromium-based browsers (Chrome, Edge, and Chromecast), the SDK estimates the bitrate from the number of bytes the video element has decoded.
  • In Firefox and Safari, which do not report these numbers, the SDK uses the bitrate from the player's API (Video.js, Shaka Player, THEOplayer) or estimates it from the size of the downloaded video segments (plain HTML5 video). For this, load the SDK before the player, as described in the installation guide.

When Data Is Sent

While the video plays, the SDK sends the collected data every 10 seconds, and right away when the video pauses, stalls, or seeks. The first video quality report is sent about 5 seconds after the playback starts. After that, each report covers 10 seconds of playback.

When the page is hidden, for example when the user switches to another tab or app, the SDK sends all data collected so far.

Debug Logging

The Video.js, Shaka Player, and THEOplayer adapters provide a surfmeter.enableDebugLogs() function. Call it before creating the analytics instance to see detailed logs in the browser console:

surfmeter.enableDebugLogs('trace'); // 'trace' | 'debug' | 'info' | 'warn' | 'error'

Ending a Session

Each session ends only once. When it ends, the SDK sends the final report. A session ends when:

  • the video plays to the end (finished, with the reason videoPlaybackEnded)
  • you call handler.destroy() (finished, with the reason destroyed)
  • you call handler.abort(reason) (aborted, with the given reason)
  • the player reports an error, or you call handler.onPlaybackError() (aborted, with the reason playbackError)
  • the user closes the page or navigates away (aborted, with the reason pageHide). If the browser keeps the page in its back/forward cache, the session continues, and the SDK only sends the data collected so far.
  • the player is disposed (Video.js), destroyed (THEOplayer), or destroyed or detached from its video element (Shaka Player), which has the same effect as handler.destroy()

A session that ends before the video started playing is sent only if the playback was attempted, that is, if the video element fired a play event. This way, video start failures and exits before playback are counted. The time of the first play attempt is used as the start time of such a session.

Call handler.destroy() when you are done with the player:

handler.destroy();
player.dispose(); // or equivalent player cleanup

To end the session as aborted instead, for example when the user leaves the video, call abort() with a reason:

handler.abort('userLeft');