> ## 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.

# CTV Video Tracking

> Best practices for tracking video events across all CTV platforms

<CardGroup cols={3}>
  <Card title="Event Schema" href="#video-event-schema" icon="list" />

  <Card title="Implementation" href="#platform-implementation" icon="code" />

  <Card title="Best Practices" href="#best-practices" icon="check" />
</CardGroup>

## Overview

Consistent video event tracking across CTV platforms enables unified audience analytics and cross-platform cohort building. This guide covers the standard video event schema and implementation patterns for all supported platforms.

## Video Event Schema

Permutive uses a consistent video event schema across all CTV platforms:

### Core Events

| Event                 | Description                   | Trigger                                   |
| --------------------- | ----------------------------- | ----------------------------------------- |
| **Videoview**         | User initiated video playback | When video starts or user selects content |
| **VideoCompletion**   | User finished watching        | When video ends or user exits             |
| **VideoAdView**       | Video ad started playing      | When ad playback begins                   |
| **VideoAdCompletion** | Video ad finished             | When ad playback ends                     |
| **VideoAdClicked**    | User clicked video ad         | When user interacts with ad               |

### Standard Video Properties

Use these properties consistently across events for unified analytics:

<Info>
  **Property Naming:** The schema uses `snake_case` property names. Native SDKs (tvOS, Android TV) use `camelCase` in their APIs (e.g., `seasonNumber`), which is automatically mapped to the `snake_case` schema.
</Info>

| Property               | Type      | Description                                   |
| :--------------------- | :-------- | :-------------------------------------------- |
| `title`                | string    | Video title                                   |
| `genre`                | string\[] | List of genres (e.g., \["Drama", "Thriller"]) |
| `content_type`         | string\[] | Content types (e.g., \["Series", "Episode"])  |
| `age_rating`           | string    | Age rating (e.g., "PG-13", "TV-MA")           |
| `runtime`              | number    | Runtime in seconds                            |
| `country`              | string    | Origin country                                |
| `original_language`    | string    | Original language code                        |
| `audio_language`       | string    | Audio language being watched                  |
| `subtitles.enabled`    | boolean   | Whether subtitles are enabled                 |
| `subtitles.language`   | string    | Subtitle language                             |
| `season_number`        | number    | Season number (for series)                    |
| `episode_number`       | number    | Episode number (for series)                   |
| `consecutive_episodes` | number    | Consecutive episodes watched                  |
| `iab_categories`       | string\[] | IAB content taxonomy categories               |

### Completion Event Properties

Additional properties for `VideoCompletion`:

| Property       | Type   | Description                    |
| :------------- | :----- | :----------------------------- |
| `completion`   | number | Percentage watched (0.0 - 1.0) |
| `engaged_time` | number | Time spent watching in seconds |

### Ad Event Properties

Properties for video ad events:

| Property      | Type   | Description                                |
| :------------ | :----- | :----------------------------------------- |
| `ad_id`       | string | Advertisement identifier                   |
| `ad_position` | string | Position: "preroll", "midroll", "postroll" |
| `ad_duration` | number | Ad duration in seconds                     |
| `campaign_id` | string | Campaign identifier                        |
| `creative_id` | string | Creative identifier                        |

***

## Platform Implementation

<Tabs>
  <Tab title="Web CTV">
    **Platforms:** Samsung Tizen, LG WebOS, HbbTV

    Use the CTV addon for automatic event tracking:

    ```javascript theme={"dark"}
    // Initialize with video properties
    permutive.addon("ctv", {
      duration: 3600000, // milliseconds
      videoProperties: {
        title: "Show Title - S1E1",
        genre: ["Drama", "Thriller"],
        season_number: 1,
        episode_number: 1,
        runtime: 3600
      }
    })

    // Sync with player events
    videoElement.addEventListener("play", () => {
      permutive.addons.ctv.play(videoElement.currentTime * 1000)
    })

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

    // Track completion
    const onVideoEnd = () => {
      permutive.addons.ctv.stop(videoElement.currentTime * 1000)
    }

    // Track ads manually
    const onAdStart = (ad) => {
      permutive.addons.ctv.track("VideoAdView", {
        ad_id: ad.id,
        ad_position: ad.position
      })
    }
    ```

    **Automatic Events:**

    * `Videoview` - tracked when addon is initialized
    * `VideoCompletion` - tracked when `.stop()` is called

    See [Web CTV documentation](/sdks/ctv/web-ctv#ctv-addon) for full details.
  </Tab>

  <Tab title="tvOS">
    **Platform:** tvOS

    Use MediaTracker for video tracking:

    ```swift theme={"dark"}
    import Permutive_iOS

    // Create MediaTracker when video starts
    mediaTracker = try? Permutive.shared.createMediaTracker(
        durationMilliseconds: videoDurationMs,
        videoProperties: MediaTracker.VideoProperties(
            title: "Show Title - S1E1",
            genre: ["Drama", "Thriller"],
            contentType: ["Series", "Episode"]
        ),
        pageProperties: MediaTracker.PageProperties(
            title: "Now Playing",
            url: URL(string: "https://example.com/show/s1e1")
        )
    )

    // Sync with player
    player.observe(\.timeControlStatus) { player, _ in
        switch player.timeControlStatus {
        case .playing:
            try? mediaTracker?.play()
        case .paused:
            mediaTracker?.pause()
        default:
            break
        }
    }

    // Track completion
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        mediaTracker?.stop()
    }
    ```

    **Automatic Events:**

    * `VideoPlay` - tracked on first `play()` call
    * `VideoPause` - tracked on `pause()` call
    * `VideoComplete` - tracked on `stop()` call

    See [tvOS documentation](/sdks/ctv/tvos#video-tracking) for full details.
  </Tab>

  <Tab title="Android TV">
    **Platforms:** Android TV, Google TV

    Use MediaTracker for video tracking:

    ```kotlin theme={"dark"}
    import com.permutive.android.MediaTracker

    // Create MediaTracker when video starts
    videoTracker = permutive.trackVideoView(
        durationMilliseconds = videoDurationMs,
        videoProperties = MediaTracker.VideoProperties(
            title = "Show Title - S1E1",
            genre = listOf("Drama", "Thriller"),
            contentType = listOf("Series", "Episode"),
            seasonNumber = 1,
            episodeNumber = 1
        ),
        pageProperties = MediaTracker.PageProperties(
            title = "Now Playing",
            url = Uri.parse("https://example.com/show/s1e1")
        )
    )

    // Sync with ExoPlayer
    exoPlayer.addListener(object : Player.Listener {
        override fun onIsPlayingChanged(isPlaying: Boolean) {
            if (isPlaying) {
                videoTracker.play()
            } else {
                videoTracker.pause()
            }
        }
    })

    // Track completion
    override fun onDestroy() {
        super.onDestroy()
        videoTracker.stop()
    }
    ```

    **Automatic Events:**

    * `Videoview` - tracked when MediaTracker is created
    * `VideoComplete` - tracked on `stop()` call

    See [Android TV documentation](/sdks/ctv/android-tv#video-tracking) for full details.
  </Tab>

  <Tab title="Roku">
    **Platform:** Roku

    Track events manually through the Permutive Task:

    ```brightscript theme={"dark"}
    ' Generate view ID for this content session
    m.global.permutive.view_id = CreateObject("roDeviceInfo").GetRandomUUID()

    ' Track video start
    m.global.permutive.event = {
        name: "Videoview"
        properties: {
            title: m.video.title
            genre: m.video.genre
            season_number: m.video.season
            episode_number: m.video.episode
            runtime: m.video.duration
        }
    }

    ' Track video completion
    sub onVideoComplete()
        m.global.permutive.event = {
            name: "VideoCompletion"
            properties: {
                title: m.video.title
                completion: m.player.position / m.player.duration
                engaged_time: m.engagedSeconds
            }
        }
    end sub

    ' Track ad events
    sub onAdStart(ad as Object)
        m.global.permutive.event = {
            name: "VideoAdView"
            properties: {
                ad_id: ad.id
                ad_position: ad.position
            }
        }
    end sub
    ```

    **Manual Events:** All events must be tracked explicitly on Roku.

    See [Roku documentation](/sdks/ctv/roku#event-tracking) for full details.
  </Tab>
</Tabs>

***

## Engagement Tracking

### How Engagement Works

Engagement time measures how long a user actively watches content:

1. **Start** - When `play()` is called, engagement timer starts
2. **Pause** - When `pause()` is called, timer pauses
3. **Resume** - When `play()` is called again, timer resumes
4. **Complete** - When `stop()` is called, total engagement is recorded

### Buffering Considerations

Buffering should pause engagement tracking:

<Tabs>
  <Tab title="Web CTV">
    ```javascript theme={"dark"}
    videoElement.addEventListener("waiting", () => {
      permutive.addons.ctv.pause()
    })

    videoElement.addEventListener("playing", () => {
      permutive.addons.ctv.play()
    })
    ```
  </Tab>

  <Tab title="Native (iOS/Android)">
    ```kotlin theme={"dark"}
    // Android example
    exoPlayer.addListener(object : Player.Listener {
        override fun onPlaybackStateChanged(state: Int) {
            when (state) {
                Player.STATE_BUFFERING -> videoTracker.pause()
                Player.STATE_READY -> {
                    if (exoPlayer.isPlaying) videoTracker.play()
                }
            }
        }
    })
    ```
  </Tab>
</Tabs>

### Seeking/Scrubbing

When users seek to a new position:

```javascript theme={"dark"}
// Web CTV
videoElement.addEventListener("seeked", () => {
  permutive.addons.ctv.play(videoElement.currentTime * 1000)
})
```

```kotlin theme={"dark"}
// Android
exoPlayer.addAnalyticsListener(object : AnalyticsListener {
    override fun onPositionDiscontinuity(
        eventTime: AnalyticsListener.EventTime,
        reason: Int
    ) {
        if (reason == Player.DISCONTINUITY_REASON_SEEK) {
            videoTracker.play(exoPlayer.currentPosition)
        }
    }
})
```

***

## Ad Break Handling

### Pre-roll Ads

Track ads before main content:

```javascript theme={"dark"}
// Web CTV example
const onPrerollStart = (ad) => {
  permutive.addons.ctv.track("VideoAdView", {
    ad_id: ad.id,
    ad_position: "preroll",
    ad_duration: ad.duration
  })
}

const onPrerollEnd = (ad) => {
  permutive.addons.ctv.track("VideoAdCompletion", {
    ad_id: ad.id
  })

  // Now start main content
  permutive.addon("ctv", {
    duration: videoDuration,
    videoProperties: { title: "Show Title" }
  })
  permutive.addons.ctv.play()
}
```

### Mid-roll Ads

Pause main content tracking during ads:

```javascript theme={"dark"}
const onMidrollStart = (ad) => {
  // Pause main content tracking
  permutive.addons.ctv.pause()

  // Track ad
  permutive.addons.ctv.track("VideoAdView", {
    ad_id: ad.id,
    ad_position: "midroll"
  })
}

const onMidrollEnd = (ad) => {
  permutive.addons.ctv.track("VideoAdCompletion", {
    ad_id: ad.id
  })

  // Resume main content tracking
  permutive.addons.ctv.play()
}
```

### Post-roll Ads

Track ads after main content completes:

```javascript theme={"dark"}
const onContentEnd = () => {
  // Complete main content tracking first
  permutive.addons.ctv.stop()
}

const onPostrollStart = (ad) => {
  permutive.addons.ctv.track("VideoAdView", {
    ad_id: ad.id,
    ad_position: "postroll"
  })
}
```

***

## Best Practices

<Tabs>
  <Tab title="Do">
    * **Set duration accurately** - Provide video duration in milliseconds when initializing
    * **Sync with player state** - Call `play()`/`pause()` when the player state changes
    * **Always call stop()** - Ensure `stop()` is called when video ends or user exits
    * **Handle buffering** - Pause tracking during buffering states
    * **Track all ad events** - Capture ad views and completions for ad analytics
    * **Use consistent properties** - Follow the standard schema across all platforms
    * **Include video metadata** - Richer metadata enables more precise cohort building
    * **Generate unique view IDs** - Create new view IDs per content session (Roku)
  </Tab>

  <Tab title="Don't">
    * Don't forget to call `stop()` when done
    * Don't create multiple trackers simultaneously
    * Don't skip duration - completion percentage requires it
    * Don't track engagement during ads (pause main tracker)
    * Don't use inconsistent property names across platforms
    * Don't send PII in video properties
    * Don't guess at video duration - wait for it to be available
  </Tab>
</Tabs>

***

## Cross-Platform Consistency

For unified analytics across platforms, ensure:

### Event Name Consistency

| Platform   | Videoview Event      | Completion Event           | Ad View Event          |
| ---------- | -------------------- | -------------------------- | ---------------------- |
| Web CTV    | `Videoview`          | `VideoCompletion`          | `VideoAdView`          |
| tvOS       | `VideoPlay`          | `VideoComplete`            | Manual                 |
| Android TV | `Videoview`          | `VideoComplete`            | Manual                 |
| Roku       | `Videoview` (manual) | `VideoCompletion` (manual) | `VideoAdView` (manual) |

### Property Mapping

Ensure property names are consistent:

| Web CTV          | iOS SDK         | Android SDK     | Roku             |
| ---------------- | --------------- | --------------- | ---------------- |
| `title`          | `title`         | `title`         | `title`          |
| `genre`          | `genre`         | `genre`         | `genre`          |
| `season_number`  | `seasonNumber`  | `seasonNumber`  | `season_number`  |
| `episode_number` | `episodeNumber` | `episodeNumber` | `episode_number` |

<Info>
  Work with [Technical Services](mailto:technical-services@permutive.com) to configure a unified event schema across all platforms.
</Info>

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Engagement time is incorrect">
    **Problem:** Engaged time doesn't match expected values.

    **Solutions:**

    * Ensure `play()` is called only when video is actually playing
    * Call `pause()` during buffering and paused states
    * Verify `stop()` is called when video ends
    * Check that ad breaks pause the main content tracker
  </Accordion>

  <Accordion title="Completion percentage is 0 or missing">
    **Problem:** VideoCompletion event shows 0% or null completion.

    **Solutions:**

    * Provide duration when creating the tracker/addon
    * Ensure duration is in the correct unit (milliseconds for most SDKs)
    * Wait for video metadata to load before creating tracker
  </Accordion>

  <Accordion title="Video events not appearing in dashboard">
    **Problem:** Events don't show in Permutive analytics.

    **Solutions:**

    * Verify SDK is initialized with correct credentials
    * Check browser/device console for errors
    * Ensure events match your workspace schema
    * Wait 5-10 minutes for events to process
  </Accordion>

  <Accordion title="Ad events not linked to video sessions">
    **Problem:** Ad events aren't associated with video content.

    **Solutions:**

    * Track ads using the same addon/tracker instance
    * On Roku, use the same `view_id` for content and ads
    * Ensure ad events fire during the video session
  </Accordion>
</AccordionGroup>

***

## Related Documentation

<CardGroup cols={2}>
  <Card title="CTV Overview" icon="tv" href="/sdks/ctv/overview">
    Platform selection guide
  </Card>

  <Card title="Web CTV" icon="browser" href="/sdks/ctv/web-ctv">
    Tizen, WebOS, HbbTV integration
  </Card>

  <Card title="tvOS" icon="apple" href="/sdks/ctv/tvos">
    tvOS integration
  </Card>

  <Card title="Android TV" icon="android" href="/sdks/ctv/android-tv">
    Android TV integration
  </Card>

  <Card title="Roku" icon="tv" href="/sdks/ctv/roku">
    Roku integration
  </Card>

  <Card title="iOS Video Tracking" icon="video" href="/sdks/mobile/ios/features/video-tracking">
    Detailed iOS MediaTracker docs
  </Card>
</CardGroup>
