Video Tracking

Track video playback with per-minute active time, engagement, and performance metrics.

Web only
Video tracking via player adapters is currently available for the JavaScript web collector only.

Overview

Video tracking creates dedicated videoplay events, independent from pageviews. Each tracked video gets its own session with:

  • Active time — per-minute breakdown of actual play time (only counted when the page is visible and focused)
  • Engagement percent — percentage of the video watched (currentTime / duration)
  • Performance metrics — startup time, rebuffering, load errors
  • Heartbeat — periodic updates every 30 seconds so partial views are never lost

Auto-detection

The simplest way to track JWPlayer instances. Add autoDetectVideoPlayers to your setConfig() call and the SDK automatically finds and tracks all players on the page, including those added dynamically.

alkeAnalytics.setConfig({
    propertyId: 'your-property-uuid',
    autoDetectVideoPlayers: ['jwplayer']
});
ParameterTypeDescription
autoDetectVideoPlayersstring[]optionalList of player names to auto-detect. Currently supported: `'jwplayer'`

Auto-detection scans the page for player instances every 500ms for 30 seconds after initialization, then stops. Players added after this window can be tracked manually.

Manual Tracking

Use trackVideo() when you need direct control — for example, after a dynamic player load or to track a custom player.

import JWPlayerAdapter from 'alke-collector/players/jwplayer';

// Create an adapter for your player instance
const adapter = new JWPlayerAdapter(jwplayer('my-player'));

// Start tracking — returns a handle
const handle = alkeAnalytics.trackVideo(adapter);

// Stop tracking early (sends a final event and cleans up)
handle.destroy();

// Access the unique ID generated for this video session
const videoId = handle.getVideoId();
ParameterTypeDescription
playerAdapterVideoPlayerAdapterrequiredAn adapter object implementing the player interface (see below)
bindingKeystringoptionalOptional player identifier (typically the player container element id) used to reuse a video event UUID reserved earlier via [`getVideoAdTargeting()`](#video-ad-attribution-gam). See that section for details.

Return value: an object with two methods:

MethodDescription
destroy()Sends a final event and stops all tracking for this video
getVideoId()Returns the unique ID (string) generated for this video session

JWPlayer Adapter

The built-in JWPlayerAdapter wraps a JWPlayer instance and implements the full player interface automatically, including startup time measurement from firstFrame and playlist change handling.

import JWPlayerAdapter from 'alke-collector/players/jwplayer';

jwplayer('my-player').setup({ file: 'video.mp4' });

// Pass the JWPlayer instance to the adapter
const adapter = new JWPlayerAdapter(jwplayer('my-player'));
const handle = alkeAnalytics.trackVideo(adapter);

When using autoDetectVideoPlayers: ['jwplayer'], this adapter is created automatically for each detected player — no manual instantiation needed.

Custom Player Adapter

To track any other player, implement the VideoPlayerAdapter interface:

const myAdapter = {
    // Video metadata
    getName()        { return player.getTitle(); },
    getDuration()    { return player.totalDuration; },
    getCurrentTime() { return player.currentPosition; },
    getVideoUrl()    { return player.sourceUrl; },
    getVideoType()   { return 'ondemand'; }, // 'ondemand' | 'linear' | null

    // Event registration
    onPlay(callback)       { player.on('play', callback); },
    onPause(callback)      { player.on('pause', callback); },
    onComplete(callback)   { player.on('ended', callback); },
    onBuffer(callback)     { player.on('waiting', callback); },
    onError(callback)      { player.on('error', callback); },
    onFirstFrame(callback) { player.on('playing', () => callback({ loadTime: null })); },

    // Cleanup
    destroy() {
        player.off('play'); player.off('pause'); player.off('ended');
        player.off('waiting'); player.off('error'); player.off('playing');
    }
};

alkeAnalytics.trackVideo(myAdapter);
ParameterTypeDescription
getName()function → stringrequiredReturns the video title displayed in reports
getDuration()function → numberrequiredReturns total duration in seconds (`0` if unknown)
getCurrentTime()function → numberrequiredReturns current playback position in seconds
getVideoUrl()function → stringrequiredReturns the asset URL (e.g., `.mp4`, HLS manifest)
getVideoType()function → stringrequiredReturns `'ondemand'`, `'linear'`, or `null`
onPlay(callback)functionrequiredRegisters a callback for play/resume events
onPause(callback)functionrequiredRegisters a callback for pause events
onComplete(callback)functionrequiredRegisters a callback for natural video completion
onBuffer(callback)functionrequiredRegisters a callback for buffering/stall events
onError(callback)functionrequiredRegisters a callback for fatal playback errors
onFirstFrame(callback)functionrequiredRegisters a callback for the first frame rendered. The callback receives `{ loadTime: number|null }` where `loadTime` is the startup time in milliseconds.
destroy()functionrequiredRemoves all event listeners registered by this adapter

Page-level Video Metadata

For simple use cases where video metrics should be attached to the current pageview rather than tracked as a dedicated videoplay event, use these setters before the pageview is finalized:

alkeAnalytics.setVideoType('ondemand');       // 'ondemand' | 'linear'
alkeAnalytics.setVideoRebuffer(true);         // rebuffering occurred
alkeAnalytics.setVideoLoadError(false);       // no load error
alkeAnalytics.setVideoStartupTime(850);       // 850ms startup
When to use page-level metadata vs. trackVideo()
Use setVideoType() and related methods when you want to annotate a pageview with video context (e.g., a video page where startup time matters). Use trackVideo() when you need a full independent video tracking session with active time, heartbeat, and engagement.
ParameterTypeDescription
setVideoType(type)string|nulloptional`'ondemand'`, `'linear'`, or `null` to clear
setVideoRebuffer(value)boolean|nulloptional`true` if rebuffering occurred, `null` to clear
setVideoLoadError(value)boolean|nulloptional`true` if a fatal load error occurred, `null` to clear
setVideoStartupTime(ms)numberoptionalStartup time in milliseconds (positive integer)

Video Ad Attribution (GAM)

To attribute Google Ad Manager video revenue to the matching videoplay event, the VAST master tag must carry the video event’s UUID in its custom targeting — exactly like display ads carry alke_id for the pageview. Use getVideoAdTargeting() to obtain a ready-to-use custom targeting string.

// When building the VAST ad request — typically before the player is even detected
const targeting = alkeAnalytics.getVideoAdTargeting('my-player');
// → "alke_id=<videoUuid>"

adTagUrl += '&cust_params=' + encodeURIComponent(targeting);

Why a dedicated method: the ad request often fires before the player is detected and the videoplay event is created, so the video UUID does not yet exist. getVideoAdTargeting() reserves that UUID synchronously and returns it immediately. When tracking later starts for the same player, the SDK reuses the reserved UUID so the ad impression and the videoplay event share the same alke_id.

Under async loading

getVideoAdTargeting() returns its value synchronously, so it requires window.alkeAnalytics to exist. Since the collector loads asynchronously, build your ad tag from inside a command-queue callback so it never runs before the SDK is ready:

window.alkeAnalyticsCmd.push(function () {
    const targeting = window.alkeAnalytics.getVideoAdTargeting('my-player');
    // build the VAST ad tag with `targeting`, then request the ad
});

The returned string contains a single keyword:

KeywordMeaning
alke_idThe reserved UUID of the upcoming videoplay event — the join key for revenue attribution

Binding the reservation to the right player: pass the same key to both calls. The recommended key is the player container element id — the same value auto-detection uses — so the reservation is matched deterministically even with several players on the page:

const targeting = alkeAnalytics.getVideoAdTargeting('botr_player'); // container id
// ...later, manually or via auto-detection...
alkeAnalytics.trackVideo(adapter, 'botr_player');

When using autoDetectVideoPlayers: ['jwplayer'], the SDK automatically passes the detected player’s container id as the binding key — so reserving with that same id is all you need.

ParameterTypeDescription
keystringrequiredBinding key — use the **player container element id** (the same value auto-detection uses). Calls with the same key are idempotent and return the same `alke_id`. **Required**: a call without a key returns an empty string, so always pass the player's id.
Reservations reset per pageview
Reservations are cleared on every pushPageView(). Call getVideoAdTargeting(playerId) for the current page when building each ad request.

Playlists (one ad per item)

Each playlist item is tracked as a separate videoplay event with its own UUID — an ad id is never reused across items, so revenue can’t concentrate on a single event. To keep the ad request of each item in sync with its event, call getVideoAdTargeting(playerId) again for every item and rebuild your cust_params.

Read it at ad-request time — i.e. when you actually build the tag (e.g. a beforePlay handler, your header-bidding callback, or the ad-tag resolution), which happens after playlistItem. By then the SDK has already switched its session to the current item, so you get the correct alke_id:

jwplayer('botr_player').on('beforePlay', function () {
    const targeting = alkeAnalytics.getVideoAdTargeting('botr_player');
    // rebuild the item's ad tag cust_params with `targeting`, then request the ad
});
Don't read in a racing playlistItem handler
The SDK updates its current session synchronously on playlistItem. If you read getVideoAdTargeting() from your own playlistItem handler that runs before the SDK’s, you may get the previous item’s id. Read when building the ad request (beforePlay/ad-tag callback) instead — that always runs after the switch. (Manual trackVideo() integrations are unaffected, since your code controls the order.)

Event Lifecycle

Each trackVideo() call produces the following sequence of events:

trackVideo() called
     initial event sent immediately (final=0)
     heartbeat every 30s (final=0)
     on play/pause: active time updated in memory

video ends naturally / handle.destroy() / page unloads
     final event sent (final=1) with complete active time

If the video encounters a fatal error (onError), a final event is sent and tracking stops automatically.

Multiple videos
Each trackVideo() call creates an independent session with its own active time and sequence number. You can track multiple simultaneous players on the same page.