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

# Video Tracking

> Track video playback, engagement, and completion metrics

The `MediaTracker` API allows you to track video content events with Permutive. Use this for tracking video playback, engagement, and completion metrics.

<CardGroup cols={4}>
  <Card title="Basic Usage" href="#basic-usage" icon="play" />

  <Card title="Properties" href="#video-properties" icon="list" />

  <Card title="Use Cases" href="#use-cases" icon="layer-group" />

  <Card title="Ad Tracking" href="#video-ad-tracking" icon="rectangle-ad" />
</CardGroup>

<Info>
  **Connected TV Integration**

  Video event tracking described below is available once the connected TV integration has been enabled. Please contact your Customer Success Manager (CSM) to enable this feature.
</Info>

## Overview

`MediaTracker` provides comprehensive video tracking capabilities:

* **Automatic lifecycle tracking** - Tracks Videoview and VideoCompletion events
* **Engagement metrics** - Measures time spent watching and completion percentage
* **Custom events** - Track custom video-related events
* **Rich metadata** - Support for extensive video properties

***

## Video Events

MediaTracker automatically tracks two event types:

### 1. Videoview Event

Tracked when MediaTracker is created:

```
Event: Videoview
Properties:
  - [your custom properties]
  - [standard video properties]
```

### 2. VideoCompletion Event

Tracked when video is stopped:

```
Event: VideoCompletion
Properties:
  - aggregations.VideoEngagement.engaged_time: Total time spent watching (seconds)
  - aggregations.VideoEngagement.completion: Percentage of video viewed (0.0 - 1.0)
  - [your custom properties]
  - [standard video properties]
```

> 📘 Note
>
> The `aggregations.VideoEngagement.completion` property requires a known duration to be set. Pass `duration` (in seconds) when calling `createVideoTracker(...)`.

***

## Basic Usage

### Creating a Video Tracker

Create a video tracker when playback begins. The iOS SDK does not auto-nest publisher properties under `video` like Android does — hand-build the nested `EventProperties` dictionary so the emitted event matches the canonical schema (`video.title`, `video.content_type`, …). See [CTV Video Tracking](/sdks/ctv/video-tracking#video-event-schema) for the full schema.

<CodeGroup>
  ```swift Swift theme={"dark"}
  import Permutive_iOS
  import AVKit

  class VideoPlayerViewController: UIViewController {
      private var mediaTracker: (NSObject & MediaTrackerProtocol)?
      private var player: AVPlayer!

      override func viewDidLoad() {
          super.viewDidLoad()

          let videoProps = try? EventProperties([
              "video": [
                  "title": "Sample Video",
                  "genre": ["Documentary"],
                  "content_type": ["Education"],
                  "runtime": 120
              ]
          ])

          // `duration` is a TimeInterval (seconds), not milliseconds.
          mediaTracker = try? Permutive.shared.createVideoTracker(
              duration: 120,  // 2 minutes
              properties: videoProps
          )
      }

      override func viewWillDisappear(_ animated: Bool) {
          super.viewWillDisappear(animated)
          mediaTracker?.stop()  // Important: Always stop to send VideoCompletion
      }
  }
  ```

  ```objectivec Objective-C theme={"dark"}
  #import "VideoPlayerViewController.h"
  @import Permutive_iOS;
  @import AVKit;

  @interface VideoPlayerViewController ()
  @property (nonatomic, strong) NSObject<PermutiveMediaTrackerProtocol> *mediaTracker;
  @property (nonatomic, strong) AVPlayer *player;
  @end

  @implementation VideoPlayerViewController

  - (void)viewDidLoad {
      [super viewDidLoad];

      NSError *error = nil;
      PermutiveEventProperties *videoProps = [[PermutiveEventProperties alloc]
          init:@{
              @"video": @{
                  @"title": @"Sample Video",
                  @"genre": @[@"Documentary"],
                  @"content_type": @[@"Education"],
                  @"runtime": @120
              }
          }
          error:&error];

      // duration is in seconds.
      self.mediaTracker = [Permutive.shared
          createVideoTrackerWithDuration:120
          properties:videoProps
          context:nil
          error:&error];
  }

  - (void)viewWillDisappear:(BOOL)animated {
      [super viewWillDisappear:animated];
      [self.mediaTracker stop];  // Important: Always stop to send VideoCompletion
  }

  @end
  ```
</CodeGroup>

***

## MediaTracker Lifecycle

### Expected Usage Flow

1. **Create** MediaTracker instance with properties
2. **Call** `play()` when playback begins
3. **Track buffering** with `pause()` / `play()`
4. **Track scrubbing** with `play(position)`
5. **Call** `stop()` when playback completes

> ⚠️ Single Instance Limitation
>
> Only a single instance of `PageTracker` or `MediaTracker` is available at any time. Creating a new tracker automatically stops any existing one.

### Playback Control Methods

#### play()

Call when video starts playing:

```swift theme={"dark"}
mediaTracker?.play()
```

#### play(position:)

Call when playback position changes (e.g., seeking). Position is a `TimeInterval` in seconds.

```swift theme={"dark"}
mediaTracker?.play(position: 30)  // Seek to 30 seconds
```

#### pause()

Call when video is paused or buffering:

```swift theme={"dark"}
mediaTracker?.pause()
```

#### stop()

Call when video playback completes or user exits:

```swift theme={"dark"}
mediaTracker?.stop()  // Sends VideoCompletion event
```

#### set(duration:)

Call to update the video duration if it wasn't known when the tracker was created:

```swift theme={"dark"}
mediaTracker?.set(duration: 240)  // 4 minutes
```

> ⚠️ Important: Always Call stop()
>
> Failing to call `stop()` will prevent VideoCompletion events from being tracked.

***

## Video Properties

The iOS SDK accepts an untyped `EventProperties` dictionary. Build the nested shape that matches the [canonical schema](/sdks/ctv/video-tracking#video-event-schema) — properties under a top-level `video` parent, in `snake_case`. There is no auto-mapping or auto-nesting like on Android.

### Standard Video Properties

```swift theme={"dark"}
let videoProps = try? EventProperties([
    "video": [
        "title": "Video Title",
        "genre": ["Action", "Thriller"],
        "content_type": ["Movie", "Feature"],
        "age_rating": "PG-13",
        "runtime": 7200,  // Runtime in seconds
        "country": "US",
        "original_language": "en",
        "audio_language": "en",
        "subtitles": [
            "enabled": true,
            "language": "en"
        ],
        "season_number": 1,
        "episode_number": 5,
        "consecutive_episodes": 3,
        "iab_categories": ["IAB1", "IAB2"]
    ]
])
```

### Schema Reference

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

> 💡 Best Practice
>
> Track as many properties as possible to enable richer cohort creation and insights.

***

## Page Context (Optional)

If the video is displayed within a page context (e.g., embedded in an article), pass a `Context` to associate the tracker with that page:

```swift theme={"dark"}
let videoProps = try? EventProperties([
    "video": ["title": "Tutorial Video"]
])

let context = Context(
    title: "How to Use iOS SDK",
    url: URL(string: "https://example.com/tutorial"),
    referrer: URL(string: "https://example.com/docs")
)

mediaTracker = try? Permutive.shared.createVideoTracker(
    duration: 120,
    properties: videoProps,
    context: context
)
```

### Context Reference

| Property     | Type    | Description        |
| ------------ | ------- | ------------------ |
| **title**    | String? | Page title         |
| **url**      | URL?    | Page URL           |
| **referrer** | URL?    | Referring page URL |

<Tip>
  Providing a `url` in `Context` enables contextual cohort generation for the video content.
</Tip>

***

## Custom Properties

The iOS SDK does not take a separate "custom properties" parameter — add custom keys directly to the same `EventProperties` dictionary, alongside the `video` schema fields. Custom keys are emitted at the top level of the event:

```swift theme={"dark"}
let props = try? EventProperties([
    "video": [
        "title": "Episode 5",
        "season_number": 1,
        "episode_number": 5
    ],
    "video_id": "vid_12345",
    "playlist_id": "playlist_abc",
    "is_premium_content": true,
    "content_partner": "PartnerXYZ"
])

mediaTracker = try? Permutive.shared.createVideoTracker(
    duration: 120,
    properties: props
)
```

***

## Complete Example

<CodeGroup>
  ```swift Swift theme={"dark"}
  import Permutive_iOS
  import AVKit

  class VideoPlayerViewController: UIViewController {

      private var mediaTracker: (NSObject & MediaTrackerProtocol)?
      private var player: AVPlayer!
      private var playerObserver: Any?

      override func viewDidLoad() {
          super.viewDidLoad()

          // Set up AVPlayer
          setupPlayer()
      }

      private func setupPlayer() {
          let url = URL(string: "https://example.com/video.mp4")!
          player = AVPlayer(url: url)

          let playerViewController = AVPlayerViewController()
          playerViewController.player = player
          addChild(playerViewController)
          view.addSubview(playerViewController.view)
          playerViewController.view.frame = view.bounds

          // Get duration once available, then set up tracker
          player.currentItem?.asset.loadValuesAsynchronously(forKeys: ["duration"]) { [weak self] in
              guard let self = self,
                    let duration = self.player.currentItem?.duration,
                    !duration.isIndefinite else { return }

              let durationSeconds = CMTimeGetSeconds(duration)

              DispatchQueue.main.async {
                  let props = try? EventProperties([
                      "video": [
                          "title": "Introduction to iOS",
                          "genre": ["Educational", "Technology"],
                          "content_type": ["Tutorial"],
                          "age_rating": "G",
                          "runtime": 180,
                          "country": "US",
                          "original_language": "en",
                          "audio_language": "en",
                          "subtitles": ["enabled": false],
                          "iab_categories": ["IAB19"]
                      ],
                      "video_id": "vid_tutorial_001",
                      "is_premium": false
                  ])

                  let context = Context(
                      title: "iOS Tutorial Page",
                      url: URL(string: "https://example.com/tutorials/ios"),
                      referrer: URL(string: "https://example.com/home")
                  )

                  self.mediaTracker = try? Permutive.shared.createVideoTracker(
                      duration: durationSeconds,
                      properties: props,
                      context: context
                  )
                  self.observePlayer()
              }
          }
      }

      private func observePlayer() {
          playerObserver = player.observe(\.timeControlStatus) { [weak self] player, _ in
              switch player.timeControlStatus {
              case .playing:
                  self?.mediaTracker?.play()
              case .paused:
                  self?.mediaTracker?.pause()
              case .waitingToPlayAtSpecifiedRate:
                  break // Buffering
              @unknown default:
                  break
              }
          }
      }

      override func viewWillDisappear(_ animated: Bool) {
          super.viewWillDisappear(animated)
          mediaTracker?.stop()  // Send VideoCompletion event
      }

      deinit {
          if let observer = playerObserver {
              player.removeTimeObserver(observer)
          }
      }
  }
  ```

  ```objectivec Objective-C theme={"dark"}
  @implementation VideoPlayerViewController

  - (void)viewDidLoad {
      [super viewDidLoad];
      [self setupPlayer];
  }

  - (void)setupPlayer {
      NSURL *url = [NSURL URLWithString:@"https://example.com/video.mp4"];
      self.player = [AVPlayer playerWithURL:url];

      AVPlayerViewController *playerVC = [[AVPlayerViewController alloc] init];
      playerVC.player = self.player;
      [self addChildViewController:playerVC];
      [self.view addSubview:playerVC.view];
      playerVC.view.frame = self.view.bounds;

      // Get duration once available
      [self.player.currentItem.asset loadValuesAsynchronouslyForKeys:@[@"duration"]
                                                   completionHandler:^{
          CMTime duration = self.player.currentItem.duration;
          if (CMTIME_IS_INDEFINITE(duration)) return;

          NSTimeInterval durationSeconds = CMTimeGetSeconds(duration);

          dispatch_async(dispatch_get_main_queue(), ^{
              NSError *error = nil;
              PermutiveEventProperties *props = [[PermutiveEventProperties alloc]
                  init:@{
                      @"video": @{
                          @"title": @"Introduction to iOS",
                          @"genre": @[@"Educational", @"Technology"],
                          @"content_type": @[@"Tutorial"],
                          @"age_rating": @"G",
                          @"runtime": @180,
                          @"country": @"US",
                          @"original_language": @"en",
                          @"audio_language": @"en",
                          @"subtitles": @{@"enabled": @NO},
                          @"iab_categories": @[@"IAB19"]
                      },
                      @"video_id": @"vid_tutorial_001",
                      @"is_premium": @NO
                  }
                  error:&error];

              PermutiveContext *context = [[PermutiveContext alloc]
                  initWithTitle:@"iOS Tutorial Page"
                  url:[NSURL URLWithString:@"https://example.com/tutorials/ios"]
                  referrer:[NSURL URLWithString:@"https://example.com/home"]];

              self.mediaTracker = [Permutive.shared
                  createVideoTrackerWithDuration:durationSeconds
                  properties:props
                  context:context
                  error:&error];

              [self observePlayer];
          });
      }];
  }

  - (void)observePlayer {
      [self.player addObserver:self
                    forKeyPath:@"timeControlStatus"
                       options:NSKeyValueObservingOptionNew
                       context:nil];
  }

  - (void)observeValueForKeyPath:(NSString *)keyPath
                        ofObject:(id)object
                          change:(NSDictionary *)change
                         context:(void *)context {

      if ([keyPath isEqualToString:@"timeControlStatus"]) {
          if (self.player.timeControlStatus == AVPlayerTimeControlStatusPlaying) {
              [self.mediaTracker play];
          } else if (self.player.timeControlStatus == AVPlayerTimeControlStatusPaused) {
              [self.mediaTracker pause];
          }
      }
  }

  - (void)viewWillDisappear:(BOOL)animated {
      [super viewWillDisappear:animated];
      [self.mediaTracker stop];  // Send VideoCompletion event
  }

  - (void)dealloc {
      [self.player removeObserver:self forKeyPath:@"timeControlStatus"];
  }

  @end
  ```
</CodeGroup>

***

## Tracking Custom Events

Track custom video-related events using the `track(event:properties:)` method. Both parameters are required (pass `nil` for `properties` if there are none).

```swift theme={"dark"}
// User shares video
try? mediaTracker?.track(
    event: "VideoShared",
    properties: try? EventProperties([
        "share_method": "twitter",
        "video_position_seconds": 45
    ])
)

// User adds to favorites
try? mediaTracker?.track(event: "VideoAddedToFavorites", properties: nil)

// Quality changed
try? mediaTracker?.track(
    event: "VideoQualityChanged",
    properties: try? EventProperties([
        "from_quality": "720p",
        "to_quality": "1080p"
    ])
)
```

***

## Video Ad Tracking

<Warning>
  **iOS does not have a native AdTracker.** Emit standard `VideoAdView`, `VideoAdCompletion`, and `VideoAdClicked` events using `Permutive.shared.track(event:properties:)` with properties nested under an `ad` parent per the [canonical schema](/sdks/ctv/video-tracking#ad-event-properties).
</Warning>

```swift theme={"dark"}
// When the ad starts
try? Permutive.shared.track(
    event: "VideoAdView",
    properties: try? EventProperties([
        "ad": [
            "title": "Product Ad",
            "duration": 15,
            "muted": false,
            "campaign_id": "campaign_123",
            "creative_id": "creative_456"
        ]
    ])
)

// When the ad completes
try? Permutive.shared.track(
    event: "VideoAdCompletion",
    properties: try? EventProperties([
        "ad": [
            "title": "Product Ad",
            "campaign_id": "campaign_123",
            "creative_id": "creative_456"
        ]
    ])
)
```

See [Video Ad Tracking](/sdks/mobile/ios/features/video-ad-tracking) for the full guide.

***

## Use Cases

<AccordionGroup>
  <Accordion title="Movie/TV App" icon="film">
    ```swift theme={"dark"}
    let props = try? EventProperties([
        "video": [
            "title": movie.title,
            "genre": movie.genres,
            "content_type": ["Movie", "Feature"],
            "age_rating": movie.rating,
            "runtime": movie.runtimeSeconds,
            "country": movie.country,
            "original_language": movie.language,
            "iab_categories": movie.iabCategories
        ]
    ])

    mediaTracker = try? Permutive.shared.createVideoTracker(
        duration: movieDurationSeconds,
        properties: props
    )
    ```
  </Accordion>

  <Accordion title="Educational/Tutorial App" icon="graduation-cap">
    ```swift theme={"dark"}
    let props = try? EventProperties([
        "video": [
            "title": tutorial.title,
            "genre": ["Educational"],
            "content_type": ["Tutorial"],
            "runtime": tutorial.runtimeSeconds
        ],
        "course_id": tutorial.courseId,
        "module_id": tutorial.moduleId,
        "difficulty_level": tutorial.difficulty
    ])

    mediaTracker = try? Permutive.shared.createVideoTracker(
        duration: tutorialDurationSeconds,
        properties: props
    )
    ```
  </Accordion>

  <Accordion title="Series/Episode Tracking" icon="tv">
    ```swift theme={"dark"}
    let props = try? EventProperties([
        "video": [
            "title": episode.title,
            "genre": series.genres,
            "content_type": ["Series", "Episode"],
            "season_number": episode.season,
            "episode_number": episode.number,
            "consecutive_episodes": userWatchData.consecutiveCount,
            "runtime": episode.runtimeSeconds
        ]
    ])

    mediaTracker = try? Permutive.shared.createVideoTracker(
        duration: episodeDurationSeconds,
        properties: props
    )
    ```
  </Accordion>
</AccordionGroup>

***

## tvOS Considerations

<Info>
  **tvOS Note:** MediaTracker works identically on tvOS. Use the same APIs with your TVUIKit video player or AVPlayerViewController.
</Info>

```swift theme={"dark"}
// tvOS with AVPlayerViewController
class TVVideoPlayerViewController: UIViewController {
    private var mediaTracker: MediaTrackerProtocol?

    override func viewDidLoad() {
        super.viewDidLoad()

        let playerVC = AVPlayerViewController()
        playerVC.player = AVPlayer(url: videoURL)

        present(playerVC, animated: true) {
            self.setupMediaTracker()
            playerVC.player?.play()
        }
    }
}
```

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="VideoCompletion Not Tracked">
    **Problem:** VideoCompletion events not appearing.

    **Cause:** `stop()` not called.

    **Solution:** Always call `stop()` in `viewWillDisappear`:

    ```swift theme={"dark"}
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        mediaTracker?.stop()  // REQUIRED
    }
    ```
  </Accordion>

  <Accordion title="Engagement Time Incorrect">
    **Problem:** Engaged time is inaccurate.

    **Causes:**

    1. Not calling `play()` when video plays
    2. Not calling `pause()` when video pauses/buffers

    **Solution:** Track all play/pause events:

    ```swift theme={"dark"}
    playerObserver = player.observe(\.timeControlStatus) { [weak self] player, _ in
        switch player.timeControlStatus {
        case .playing:
            try? self?.mediaTracker?.play()
        case .paused:
            self?.mediaTracker?.pause()
        default:
            break
        }
    }
    ```
  </Accordion>

  <Accordion title="Completion Percentage Missing">
    **Problem:** `aggregations.VideoEngagement.completion` is missing or nil.

    **Cause:** Duration not provided when creating the video tracker.

    **Solution:** Always provide `duration` (in seconds):

    ```swift theme={"dark"}
    mediaTracker = try? Permutive.shared.createVideoTracker(
        duration: videoDurationSeconds  // REQUIRED for completion
    )
    ```
  </Accordion>

  <Accordion title="Multiple Tracker Error">
    **Problem:** Error about multiple trackers.

    **Cause:** Creating a new PageTracker or video tracker without closing the existing one.

    **Solution:** Close the existing tracker first:

    ```swift theme={"dark"}
    // Close old tracker
    mediaTracker?.stop()

    // Now create new tracker
    mediaTracker = try? Permutive.shared.createVideoTracker(...)
    ```

    See [Common Errors](/sdks/mobile/ios/troubleshooting/common-errors) for more troubleshooting.
  </Accordion>
</AccordionGroup>

***

## Best Practices

<Tabs>
  <Tab title="Do">
    * Always call `stop()` when video finishes
    * Provide duration for completion tracking
    * Track play/pause events accurately
    * Include as many video properties as possible
    * Close tracker before creating a new one
    * Track buffering with pause/play
  </Tab>

  <Tab title="Don't">
    * Forget to call `stop()`
    * Create multiple MediaTrackers simultaneously
    * Skip play/pause lifecycle events
    * Omit video duration
  </Tab>
</Tabs>

***

## Related Documentation

<CardGroup cols={2}>
  <Card title="Video Ad Tracking" icon="rectangle-ad" href="/sdks/mobile/ios/features/video-ad-tracking">
    Track video advertisements
  </Card>

  <Card title="Page Tracking" icon="file" href="/sdks/mobile/ios/features/page-tracking">
    Track non-video content
  </Card>

  <Card title="Event Properties" icon="list" href="/sdks/mobile/ios/core-concepts/event-properties">
    Custom properties
  </Card>

  <Card title="Issues" icon="wrench" href="/sdks/mobile/ios/troubleshooting/common-errors">
    Solve common issues
  </Card>
</CardGroup>

***

## API Reference

### MediaTrackerProtocol

* `play()` — Start/resume video playback
* `play(position: TimeInterval)` — Start at a specific position (seconds)
* `pause()` — Pause video playback
* `stop()` — Complete video tracking (sends `VideoCompletion`)
* `set(duration: TimeInterval)` — Update video duration (seconds), if it wasn't known at creation
* `track(event: String, properties: EventProperties?) throws` — Track a custom event

### Creating a Video Tracker

* `Permutive.shared.createVideoTracker(duration: TimeInterval, properties: EventProperties?, context: Context?) throws -> NSObject & MediaTrackerProtocol`
