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

# tvOS

> Integrate Permutive on tvOS using the iOS SDK

<CardGroup cols={3}>
  <Card title="Quick Start" href="/sdks/mobile/ios/getting-started/quick-start" icon="rocket" />

  <Card title="Video Tracking" href="#video-tracking" icon="video" />

  <Card title="iOS SDK Reference" href="/sdks/mobile/ios/overview" icon="book" />
</CardGroup>

## Overview

tvOS applications use the **Permutive iOS SDK**. The same SDK that powers iOS applications fully supports tvOS 12.0 and later, providing identical APIs for tracking, identity management, and ad targeting.

<Info>
  **Same SDK, Same APIs:** The Permutive iOS SDK supports both iOS and tvOS with a unified codebase. All features documented in the [iOS SDK](/sdks/mobile/ios/overview) are available on tvOS unless otherwise noted.
</Info>

## Requirements

| Requirement | Version |
| ----------- | ------- |
| tvOS        | 12.0+   |
| Xcode       | 13.0+   |
| Swift       | 5.0+    |

## Installation

<Tabs>
  <Tab title="Swift Package Manager">
    Add the package URL in Xcode:

    ```
    https://github.com/permutive-engineering/permutive-ios-spm
    ```

    Select `Permutive_iOS` as the package product.
  </Tab>

  <Tab title="CocoaPods">
    ```ruby theme={"dark"}
    target 'YourTVApp' do
      platform :tvos, '12.0'
      pod 'Permutive_iOS', '~> 2.2.0'
    end
    ```
  </Tab>
</Tabs>

## Initialization

Initialize the SDK in your app delegate or app entry point:

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

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {

        let options = Options(
            apiKey: "<your api key>",
            organisationId: "<your org id>"
        )

        do {
            try Permutive.configure(options)
        } catch {
            print("Failed to configure Permutive: \(error)")
        }

        return true
    }
}
```

For detailed initialization options, see [iOS Initialization](/sdks/mobile/ios/getting-started/initialization).

***

## Video Tracking

Video tracking on tvOS uses the same `MediaTracker` API as iOS. This is the primary use case for CTV applications.

### Creating a MediaTracker

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

class VideoPlayerViewController: UIViewController {
    private var mediaTracker: MediaTrackerProtocol?
    private var player: AVPlayer!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Get video duration in milliseconds
        let videoDurationMs = Int64(video.duration * 1000)

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

### Integrating with AVPlayer

tvOS applications typically use `AVPlayerViewController` for video playback:

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

class TVVideoPlayerViewController: UIViewController {
    private var mediaTracker: MediaTrackerProtocol?
    private var player: AVPlayer!
    private var playerObserver: Any?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupPlayer()
        setupMediaTracker()
        observePlayer()
    }

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

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

    private func setupMediaTracker() {
        player.currentItem?.asset.loadValuesAsynchronously(forKeys: ["duration"]) { [weak self] in
            guard let self = self,
                  let duration = self.player.currentItem?.duration,
                  !duration.isIndefinite else { return }

            let durationMs = Int64(CMTimeGetSeconds(duration) * 1000)

            DispatchQueue.main.async {
                self.mediaTracker = try? Permutive.shared.createMediaTracker(
                    durationMilliseconds: durationMs,
                    videoProperties: MediaTracker.VideoProperties(
                        title: "Video Title"
                    ),
                    pageProperties: nil
                )
            }
        }
    }

    private func observePlayer() {
        playerObserver = player.observe(\.timeControlStatus) { [weak self] player, _ in
            switch player.timeControlStatus {
            case .playing:
                try? 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()
    }
}
```

### MediaTracker Lifecycle

| Method    | Effect                           | Events Generated         |
| --------- | -------------------------------- | ------------------------ |
| `play()`  | Starts/resumes playback tracking | `VideoPlay` (first call) |
| `pause()` | Pauses tracking                  | `VideoPause`             |
| `stop()`  | Ends tracking permanently        | `VideoComplete`          |

<Warning>
  **Always call `stop()`:** Failing to call `stop()` when the video ends or the user exits will prevent `VideoComplete` events from being tracked.
</Warning>

For complete MediaTracker documentation, see [iOS Video Tracking](/sdks/mobile/ios/features/video-tracking).

***

## Video Ad Tracking

Track video advertisements within your content:

```swift theme={"dark"}
// When ad starts
let adTracker = try? mediaTracker?.createAdTracker(
    durationMilliseconds: 30000, // 30 second ad
    adProperties: AdTracker.AdProperties(
        title: "Advertisement",
        campaignId: "campaign_123"
    )
)

try? adTracker?.play()

// When ad completes
adTracker?.stop()
```

See [iOS Video Ad Tracking](/sdks/mobile/ios/features/video-ad-tracking) for details.

***

## Identity Management

Set user identities for cross-device tracking:

```swift theme={"dark"}
let aliases = [
    Alias(tag: "user_id", identity: userId),
    Alias(tag: "email_sha256", identity: hashedEmail)
]
try? Permutive.shared.setIdentities(aliases: aliases)
```

See [iOS Identity Management](/sdks/mobile/ios/core-concepts/identity-management) for details.

***

## Cohorts and Targeting

### Accessing Cohorts

```swift theme={"dark"}
// Get all cohorts
let cohorts = Permutive.shared.cohorts

// Get activations for a specific ad platform
let gamActivations = Permutive.shared.activations(forPlatform: "gam")
```

### Google Ad Manager Integration

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

// bannerView is a GAMBannerView instance in your view controller
let adRequest = GAMRequest()
adRequest.customTargeting = Permutive.shared.googleCustomTargeting(
    adTargetable: mediaTracker
)
bannerView.load(adRequest)
```

See [iOS Google Ad Manager](/sdks/mobile/ios/integrations/google-ad-manager) for complete integration details.

***

## tvOS-Specific Considerations

<AccordionGroup>
  <Accordion title="Focus-Based Navigation">
    tvOS uses focus-based navigation with the Siri Remote. Ensure your tracking implementation doesn't interfere with focus handling.

    ```swift theme={"dark"}
    override func didUpdateFocus(
        in context: UIFocusUpdateContext,
        with coordinator: UIFocusAnimationCoordinator
    ) {
        // Handle focus changes
        // Tracking works independently of focus
    }
    ```
  </Accordion>

  <Accordion title="No IDFA/ATT on tvOS">
    Unlike iOS, tvOS does not support IDFA or App Tracking Transparency. Use alternative identifiers:

    * User account IDs (hashed)
    * `identifierForVendor` for anonymous tracking
    * Custom identifiers from your authentication system

    ```swift theme={"dark"}
    // Use vendor identifier
    if let vendorId = UIDevice.current.identifierForVendor?.uuidString {
        try? Permutive.shared.setIdentities(aliases: [
            Alias(tag: "vendor_id", identity: vendorId)
        ])
    }
    ```
  </Accordion>

  <Accordion title="Background Playback">
    tvOS apps may continue video playback in the background. Ensure your MediaTracker handles this:

    ```swift theme={"dark"}
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(appDidEnterBackground),
        name: UIApplication.didEnterBackgroundNotification,
        object: nil
    )

    @objc func appDidEnterBackground() {
        // Continue tracking if video plays in background
        // Or pause if playback stops
        if !player.isPlaying {
            mediaTracker?.pause()
        }
    }
    ```
  </Accordion>

  <Accordion title="Top Shelf Extensions">
    If your app has a Top Shelf extension, note that extensions run in a separate process and cannot share the Permutive SDK state with your main app.
  </Accordion>
</AccordionGroup>

***

## Best Practices

<Tabs>
  <Tab title="Do">
    * Initialize SDK early in app launch
    * Create MediaTracker when video is ready to play
    * Call `stop()` when video ends or user exits
    * Sync `play()`/`pause()` with actual player state
    * Use `identifierForVendor` or account IDs for identity
    * Include video metadata for richer cohorts
  </Tab>

  <Tab title="Don't">
    * Don't create multiple MediaTrackers simultaneously
    * Don't forget to call `stop()` when done
    * Don't expect IDFA to work on tvOS
    * Don't block the main thread with SDK calls
    * Don't send PII in event properties
  </Tab>
</Tabs>

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="SDK not initializing">
    **Problem:** Permutive.shared is nil or initialization fails.

    **Solutions:**

    * Verify API key and organization ID are correct
    * Ensure `configure()` is called before accessing `shared`
    * Check for initialization errors in the catch block
    * Enable debug logging: `options.logModes = LogMode.all`
  </Accordion>

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

    **Solutions:**

    * Verify MediaTracker was created successfully
    * Ensure `play()` is called when video starts
    * Ensure `stop()` is called when video ends
    * Wait 5-10 minutes for events to process
    * Check network connectivity on the device
  </Accordion>

  <Accordion title="Engagement time incorrect">
    **Problem:** Engagement metrics don't match expected values.

    **Solutions:**

    * Sync `play()`/`pause()` calls with actual player state
    * Handle buffering states with `pause()`
    * Provide accurate duration when creating MediaTracker
  </Accordion>
</AccordionGroup>

***

## Related Documentation

<CardGroup cols={2}>
  <Card title="iOS SDK Overview" icon="apple" href="/sdks/mobile/ios/overview">
    Complete iOS/tvOS SDK documentation
  </Card>

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

  <Card title="iOS Video Ad Tracking" icon="rectangle-ad" href="/sdks/mobile/ios/features/video-ad-tracking">
    Video advertisement tracking
  </Card>

  <Card title="CTV Video Best Practices" icon="play" href="/sdks/ctv/video-tracking">
    Cross-platform video tracking guide
  </Card>
</CardGroup>
