Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.permutive.com/llms.txt

Use this file to discover all available pages before exploring further.

Track video engagement to build video-based cohorts and understand viewing behavior.

Initialization

Player Lifecycle

Best Practices

Overview

Video tracking in the Permutive JavaScript SDK is handled by the CTV addon. The addon manages engagement timing automatically and tracks the standard Permutive video events — Videoview and VideoCompletion — without requiring manual interval logic or custom event construction. The CTV addon is the correct approach for video tracking on:
  • Standard web pages with embedded video players
  • Web-based CTV applications (Samsung Tizen, LG WebOS, HbbTV)
The CTV addon must be enabled for your project before use. Enable it in the Permutive dashboard under Settings > Integrations.

Initializing the CTV Addon

Initialize the addon when a user starts watching a video. Calling permutive.addon("ctv", ...) automatically tracks a Videoview event and prepares the addon for engagement tracking.
permutive.addon("ctv", {
  duration: 3600000, // Content duration in milliseconds
  videoProperties: {
    title: "Breaking Bad - S1E1",
    genre: ["Drama", "Thriller"],
    content_type: ["Series", "Episode"],
    age_rating: "TV-MA",
    runtime: 3600,
    season_number: 1,
    episode_number: 1,
    audio_language: "en",
    iab_categories: ["IAB1-1"]
  }
})
After initialization, the addon is available at permutive.addons.ctv.

Duration

Pass duration in milliseconds. This is required for the VideoCompletion event to include an accurate completion percentage. If the duration is not known at initialization time, you can set it later using .setDuration() before calling .stop().
// Set duration later when it becomes available
permutive.addons.ctv.setDuration(videoDurationMs)

Player Lifecycle

Map your video player’s events to the CTV addon methods. The addon handles engagement timing internally — there is no need to implement your own interval or timer logic.
Addon MethodWhen to CallEffect
.play(position?)Player starts playing or user scrubsStarts or resumes engagement timer
.pause(position?)Player pausesPauses engagement timer
.stop(position?)User exits or content endsTracks VideoCompletion with aggregated engagement
Positions are passed in milliseconds.

Generic HTML5 Video Example

// Initialize the addon when the user starts a video
permutive.addon("ctv", {
  duration: videoElement.duration * 1000,
  videoProperties: {
    title: "My Video Title",
    genre: ["Documentary"]
  }
})

videoElement.addEventListener("play", function () {
  permutive.addons.ctv.play(videoElement.currentTime * 1000)
})

videoElement.addEventListener("pause", function () {
  permutive.addons.ctv.pause(videoElement.currentTime * 1000)
})

videoElement.addEventListener("ended", function () {
  permutive.addons.ctv.stop(videoElement.currentTime * 1000)
})

Handling Duration Not Yet Available

If duration is not available when the video starts (common with live streams or when metadata loads asynchronously), initialize without it and call .setDuration() once the value is known:
videoElement.addEventListener("loadedmetadata", function () {
  permutive.addons.ctv.setDuration(videoElement.duration * 1000)
})

Handling Seek and Scrub

When a user seeks to a new position, call .play() with the new position. This correctly updates the engagement tracker’s reference point without inflating engaged time:
videoElement.addEventListener("seeked", function () {
  permutive.addons.ctv.play(videoElement.currentTime * 1000)
})

Handling Buffering

Pause engagement tracking while the player is buffering so that buffer time is not counted as active engagement:
videoElement.addEventListener("waiting", function () {
  permutive.addons.ctv.pause()
})

videoElement.addEventListener("playing", function () {
  permutive.addons.ctv.play(videoElement.currentTime * 1000)
})

Video Ad Tracking

Track video ad events using the addon’s .track() method. For mid-roll ads, pause the main content tracker while the ad plays and resume it when the ad finishes.

Ad Event Reference

EventWhen to Track
VideoAdViewWhen a video ad starts playing
VideoAdCompletionWhen a video ad finishes playing
VideoAdClickedWhen a user clicks on a video ad

Ad Event Properties

Properties for video ad events are nested under the ad key:
PropertyTypeDescription
ad.titlestringTitle of the video advert
ad.durationnumberDuration of the advert in seconds
ad.mutedbooleanWhether the advert was muted
ad.campaign_idstringCampaign identifier
ad.creative_idstringCreative identifier

Pre-roll Ads

// Track the ad before initializing the main content
const onPrerollStart = function (ad) {
  permutive.addons.ctv.track("VideoAdView", {
    ad: {
      title: ad.title,
      duration: ad.duration,
      campaign_id: ad.campaignId,
      creative_id: ad.creativeId
    }
  })
}

const onPrerollEnd = function (ad) {
  permutive.addons.ctv.track("VideoAdCompletion", {
    ad: {
      title: ad.title,
      duration: ad.duration
    }
  })

  // Now initialize the main content and start tracking
  permutive.addon("ctv", {
    duration: videoDurationMs,
    videoProperties: { title: "Show Title" }
  })
  permutive.addons.ctv.play()
}

Mid-roll Ads

const onMidrollStart = function (ad) {
  // Pause main content engagement tracking
  permutive.addons.ctv.pause()

  permutive.addons.ctv.track("VideoAdView", {
    ad: {
      title: ad.title,
      duration: ad.duration,
      campaign_id: ad.campaignId
    }
  })
}

const onMidrollEnd = function (ad) {
  permutive.addons.ctv.track("VideoAdCompletion", {
    ad: {
      title: ad.title,
      duration: ad.duration
    }
  })

  // Resume main content engagement tracking
  permutive.addons.ctv.play(videoElement.currentTime * 1000)
}

Post-roll Ads

const onContentEnd = function () {
  // Complete main content tracking before post-roll
  permutive.addons.ctv.stop()
}

const onPostrollStart = function (ad) {
  permutive.addons.ctv.track("VideoAdView", {
    ad: {
      title: ad.title,
      duration: ad.duration
    }
  })
}

Video Properties

Pass video metadata under the videoProperties key when initializing the addon. All properties are optional but richer metadata enables more precise cohort building.
PropertyTypeDescription
titlestringVideo title
genrestring[]List of genres
content_typestring[]List of content types
age_ratingstringAge rating (e.g. “PG-13”, “TV-MA”)
runtimenumberRuntime in seconds
countrystringOrigin country
original_languagestringOriginal language of the content
audio_languagestringLanguage of the audio track being watched
subtitles.enabledbooleanWhether subtitles were enabled
subtitles.languagestringSubtitle language
season_numbernumberSeason number
episode_numbernumberEpisode number
consecutive_episodesnumberNumber of episodes watched consecutively
iab_categoriesstring[]IAB content taxonomy categories
Properties are type-checked by the addon. A property with the wrong type (for example, passing audio_language as an array instead of a string) will be silently removed from the event rather than causing a schema error.

Complete Working Example

A full integration with an HTML5 video element, including ad break handling:
var videoElement = document.getElementById("my-video")
var videoDurationMs = 0

// Step 1: Capture duration when metadata loads
videoElement.addEventListener("loadedmetadata", function () {
  videoDurationMs = videoElement.duration * 1000
})

// Step 2: Initialize the CTV addon when playback begins
//         This automatically tracks the Videoview event.
videoElement.addEventListener("play", function () {
  if (!permutive.addons.ctv) {
    permutive.addon("ctv", {
      duration: videoDurationMs,
      videoProperties: {
        title: videoElement.dataset.title,
        genre: ["Drama"],
        season_number: 1,
        episode_number: 1,
        audio_language: "en"
      }
    })
  }
  permutive.addons.ctv.play(videoElement.currentTime * 1000)
})

// Step 3: Pause engagement tracking when player pauses
videoElement.addEventListener("pause", function () {
  if (permutive.addons.ctv) {
    permutive.addons.ctv.pause(videoElement.currentTime * 1000)
  }
})

// Step 4: Handle buffering
videoElement.addEventListener("waiting", function () {
  if (permutive.addons.ctv) {
    permutive.addons.ctv.pause()
  }
})
videoElement.addEventListener("playing", function () {
  if (permutive.addons.ctv) {
    permutive.addons.ctv.play(videoElement.currentTime * 1000)
  }
})

// Step 5: Handle seeking
videoElement.addEventListener("seeked", function () {
  if (permutive.addons.ctv) {
    permutive.addons.ctv.play(videoElement.currentTime * 1000)
  }
})

// Step 6: Stop tracking when content ends (tracks VideoCompletion)
videoElement.addEventListener("ended", function () {
  if (permutive.addons.ctv) {
    permutive.addons.ctv.stop(videoElement.currentTime * 1000)
  }
})

// Ad tracking helpers
function onAdStart(ad) {
  if (permutive.addons.ctv) {
    permutive.addons.ctv.pause()
    permutive.addons.ctv.track("VideoAdView", {
      ad: {
        title: ad.title,
        duration: ad.duration,
        campaign_id: ad.campaignId
      }
    })
  }
}

function onAdEnd(ad) {
  if (permutive.addons.ctv) {
    permutive.addons.ctv.track("VideoAdCompletion", {
      ad: {
        title: ad.title,
        duration: ad.duration
      }
    })
    permutive.addons.ctv.play(videoElement.currentTime * 1000)
  }
}

Engagement Tracking Patterns

The CTV addon tracks engagement automatically. You do not need to implement interval timers or milestone checks manually. The addon’s internal engagement model works as follows:
  1. Play — When .play() is called, the engagement clock starts.
  2. Pause — When .pause() is called, the clock stops. Time elapsed since the last .play() call is accumulated.
  3. Resume — When .play() is called again, the clock resumes from the current accumulated total.
  4. Stop — When .stop() is called, the final accumulated engaged_time and the calculated completion percentage are included in the VideoCompletion event automatically.
This means skipped content (via seeking) is never counted as engaged time, and buffering time can be excluded by calling .pause() during buffer states.

Best Practices

The VideoCompletion event is only tracked when .stop() is called. Make sure you call it not just when a video ends naturally, but also when a user navigates away or closes the player early.
// On natural end
videoElement.addEventListener("ended", function () {
  permutive.addons.ctv.stop(videoElement.currentTime * 1000)
})

// On user navigation / page unload
window.addEventListener("beforeunload", function () {
  if (permutive.addons.ctv) {
    permutive.addons.ctv.stop()
  }
})
The completion percentage in VideoCompletion is calculated from the duration you provide. Set duration in milliseconds at initialization, or call .setDuration() once metadata is available. Do not guess or hardcode durations.
videoElement.addEventListener("loadedmetadata", function () {
  permutive.addons.ctv.setDuration(videoElement.duration * 1000)
})
Call permutive.addon("ctv", ...) once per piece of content. Calling it again reinitializes the addon and tracks a new Videoview event. If the user watches multiple videos in a session, reinitialize for each new piece of content after calling .stop() on the previous one.
// When moving to next video
permutive.addons.ctv.stop()

permutive.addon("ctv", {
  duration: nextVideoDurationMs,
  videoProperties: { title: "Next Video Title" }
})
Call .pause() when a mid-roll ad starts so that ad playback time is not included in the main content’s engaged_time. Resume with .play() when the ad finishes.
Every property you pass in videoProperties becomes available for cohort building and targeting. At minimum, include title. Adding genre, content_type, season_number, and episode_number significantly increases the granularity of video-based cohorts.

Troubleshooting

Problem: The Videoview event is not visible in Permutive analytics.Solutions:
  • Confirm the CTV addon is enabled under Settings > Integrations in the Permutive dashboard.
  • Verify permutive.addon("ctv", ...) is being called in the browser console — it should not throw an error.
  • Check that the SDK is initialized before permutive.addon("ctv", ...) is called.
  • Allow 5-10 minutes for events to appear in the dashboard after they fire.
Problem: Calling permutive.addons.ctv.play() throws because the addon is not available.Solutions:
  • Ensure permutive.addon("ctv", ...) has been called before accessing permutive.addons.ctv.
  • Add a guard check: if (permutive.addons.ctv) { ... } before calling addon methods.
  • Confirm the CTV addon is enabled in your workspace settings — it will not be registered if it has not been enabled.
Problem: The VideoCompletion event has a completion value of 0 or is missing it.Solutions:
  • Ensure duration is passed (in milliseconds) when calling permutive.addon("ctv", ...).
  • If duration is not available at initialization, call .setDuration(ms) before .stop() is called.
  • Verify that the duration value is in milliseconds, not seconds.
Problem: The engaged_time in VideoCompletion includes buffering or paused time.Solutions:
  • Call .pause() when the player enters a buffering state (waiting event on HTML5 video).
  • Call .pause() when the player is paused by the user.
  • Verify that .pause() is being called before ad breaks start.
Problem: Multiple Videoview events fire for a single piece of content.Solutions:
  • permutive.addon("ctv", ...) fires a Videoview each time it is called. Guard the initialization so it only runs once per content session.
  • Use a flag or check permutive.addons.ctv to avoid re-initializing on repeated play events from the player.

CTV Video Tracking

Video event schema and cross-platform tracking patterns

Video Integrations

Pre-built integrations for JW Player, Brightcove, YouTube, and more

Event Tracking

General event tracking with the JavaScript SDK

Web CTV

Full CTV addon reference for web-based CTV platforms