Skip to main content

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.
Same SDK, Same APIs: The Permutive iOS SDK supports both iOS and tvOS with a unified codebase. All features documented in the iOS SDK are available on tvOS unless otherwise noted.

Requirements

RequirementVersion
tvOS12.0+
Xcode13.0+
Swift5.0+

Installation

Add the package URL in Xcode:
https://github.com/permutive-engineering/permutive-ios-spm
Select Permutive_iOS as the package product.

Initialization

Initialize the SDK in your app delegate or app entry point:
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.

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

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:
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

MethodEffectEvents Generated
play()Starts/resumes playback trackingVideoPlay (first call)
pause()Pauses trackingVideoPause
stop()Ends tracking permanentlyVideoComplete
Always call stop(): Failing to call stop() when the video ends or the user exits will prevent VideoComplete events from being tracked.
For complete MediaTracker documentation, see iOS Video Tracking.

Video Ad Tracking

Track video advertisements within your content:
// 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 for details.

Identity Management

Set user identities for cross-device tracking:
let aliases = [
    Alias(tag: "user_id", identity: userId),
    Alias(tag: "email_sha256", identity: hashedEmail)
]
try? Permutive.shared.setIdentities(aliases: aliases)
See iOS Identity Management for details.

Cohorts and Targeting

Accessing Cohorts

// Get all cohorts
let cohorts = Permutive.shared.cohorts

// Get activations for a specific ad platform
let gamActivations = Permutive.shared.activations(forPlatform: "gam")
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 for complete integration details.

tvOS-Specific Considerations

tvOS uses focus-based navigation with the Siri Remote. Ensure your tracking implementation doesn’t interfere with focus handling.
override func didUpdateFocus(
    in context: UIFocusUpdateContext,
    with coordinator: UIFocusAnimationCoordinator
) {
    // Handle focus changes
    // Tracking works independently of focus
}
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
// Use vendor identifier
if let vendorId = UIDevice.current.identifierForVendor?.uuidString {
    try? Permutive.shared.setIdentities(aliases: [
        Alias(tag: "vendor_id", identity: vendorId)
    ])
}
tvOS apps may continue video playback in the background. Ensure your MediaTracker handles this:
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()
    }
}
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.

Best Practices

  • 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

Troubleshooting

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