vastlint

VAST XML for Apple tvOS

Apple TV (4th generation and later) runs tvOS. Most streaming apps on tvOS use the Google IMA SDK for tvOS to handle VAST ad playback. The tvOS IMA SDK shares the same VAST parsing and rendering engine as the iOS SDK, with the same restrictions — notably the complete absence of VPAID support.

Quick reference

VAST versions2.0, 3.0, 4.0, 4.1, 4.2, 4.3 (via IMA SDK)
VPAIDCompletely blocked
Wrapper chain limit4 hops (IMA SDK limit)
Required media formatMP4 H.264 + AAC; HLS recommended for 4K
Companion adsNot rendered on tvOS
HTTPSRequired (App Transport Security)

Installation

The IMA SDK for tvOS requires Xcode 13 or later and tvOS 15 as the minimum deployment target. The recommended installation method is Swift Package Manager.

Swift Package Manager (recommended)

In Xcode, go to File > Add Packages and search for the IMA SDK tvOS Swift package:

https://github.com/googleads/swift-package-manager-google-interactive-media-ads-tvos

Select Up to Next Major Version for new projects.

CocoaPods

Add the following to your Podfile and run pod install --repo-update:

source 'https://github.com/CocoaPods/Specs.git'
platform :tvos, '15'

target "YourApp" do
  pod 'GoogleAds-IMA-tvOS-SDK', '~> 4.16.0'
end

Import the SDK in your view controller:

import AVFoundation
import GoogleInteractiveMediaAds
import UIKit

SDK integration

IMA client-side on tvOS involves four main components:

  • IMAAdDisplayContainer — specifies where IMA renders ad UI and measures viewability (Active View / OMID).
  • IMAAdsLoader — requests ads; reuse a single instance for the app lifecycle.
  • IMAAdsRequest — defines an individual ad request with the VAST tag URL and display container.
  • IMAAdsManager — manages ad playback and fires ad events.
class ViewController: UIViewController, IMAAdsLoaderDelegate, IMAAdsManagerDelegate {

  static let adTagURLString =
    "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/..."

  var adsLoader: IMAAdsLoader!
  var adDisplayContainer: IMAAdDisplayContainer!
  var adsManager: IMAAdsManager!
  var contentPlayhead: IMAAVPlayerContentPlayhead?
  var playerViewController: AVPlayerViewController!
  var adBreakActive = false

  override func viewDidLoad() {
    super.viewDidLoad()
    setUpContentPlayer()   // initialize AVPlayer
    setUpAdsLoader()       // create IMAAdsLoader
  }

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    requestAds()
  }

  func setUpAdsLoader() {
    adsLoader = IMAAdsLoader(settings: nil)
    adsLoader.delegate = self
  }

  func requestAds() {
    adDisplayContainer = IMAAdDisplayContainer(adContainer: self.view, viewController: self)
    let request = IMAAdsRequest(
      adTagUrl: ViewController.adTagURLString,
      adDisplayContainer: adDisplayContainer,
      contentPlayhead: contentPlayhead,
      userContext: nil)
    adsLoader.requestAds(with: request)
  }

  // MARK: - IMAAdsLoaderDelegate

  func adsLoader(_ loader: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) {
    adsManager = adsLoadedData.adsManager
    adsManager.delegate = self
    adsManager.initialize(with: nil)
  }

  func adsLoader(_ loader: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) {
    print("Error loading ads: (adErrorData.adError.message ?? "")")
    playerViewController.player?.play()  // fall back to content
  }

  // MARK: - IMAAdsManagerDelegate

  func adsManager(_ adsManager: IMAAdsManager, didReceive event: IMAAdEvent) {
    if event.type == IMAAdEventType.LOADED { adsManager.start() }
  }

  func adsManager(_ adsManager: IMAAdsManager, didReceive error: IMAAdError) {
    print("AdsManager error: (error.message ?? "")")
    playerViewController.player?.play()  // fall back to content
  }
}

Mid-roll and post-roll

For mid-roll ad breaks, the SDK needs to track the current content position. Use the IMAAVPlayerContentPlayhead helper (backed by AVPlayer) when you create the content player:

func setUpContentPlayer() {
  let contentURL = URL(string: "https://example.com/content.m3u8")!
  let player = AVPlayer(url: contentURL)
  playerViewController = AVPlayerViewController()
  playerViewController.player = player

  // Pass AVPlayer to the IMA playhead tracker
  contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: player)

  // Notify IMA when content ends (enables post-roll playback)
  NotificationCenter.default.addObserver(
    self,
    selector: #selector(contentDidFinishPlaying(_:)),
    name: .AVPlayerItemDidPlayToEndTime,
    object: player.currentItem)

  addChild(playerViewController)
  view.insertSubview(playerViewController.view, at: 0)
  playerViewController.didMove(toParent: self)
}

@objc func contentDidFinishPlaying(_ notification: Notification) {
  // Required for post-roll: tell IMA content has ended
  adsLoader.contentComplete()
}

Without calling adsLoader.contentComplete(), post-roll ads will not play.

Siri Remote and focus management

On tvOS, the Siri Remote controls focus. When an ad break starts, focus must shift to the IMA ad display container; when the ad ends, focus returns to the content player. Call setNeedsFocusUpdate() to trigger the tvOS focus engine at each transition:

func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager) {
  playerViewController.player?.pause()
  // Remove content player from view so it can't intercept remote events
  playerViewController.view.removeFromSuperview()
  playerViewController.removeFromParent()
  adBreakActive = true
  setNeedsFocusUpdate()   // hand focus to IMA ad container
}

func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager) {
  addChild(playerViewController)
  view.insertSubview(playerViewController.view, at: 0)
  playerViewController.didMove(toParent: self)
  playerViewController.player?.play()
  adBreakActive = false
  setNeedsFocusUpdate()   // return focus to content player
}

If you skip setNeedsFocusUpdate(), the Siri Remote may not control ad playback (skip, pause) correctly.

VPAID is blocked

tvOS has no JavaScript execution layer at the ad level. The IMA SDK for tvOS rejects any <MediaFile> with apiFramework="VPAID". If your tag contains only a VPAID creative, the slot goes unfilled silently.

Use OMID via <AdVerifications> for viewability measurement and include a native MP4 creative for playback.

→ vastlint rule: VAST-4.1-vpaid-in-interactive-context

Media file requirements

Provide MP4 H.264 as the baseline. For Apple TV 4K, also include an HLS stream or a high-bitrate MP4 at 1080p/4K. The IMA SDK selects the best available <MediaFile> based on bitrate and dimensions.

<MediaFiles>
  <!-- Apple TV 4K — high quality -->
  <MediaFile type="video/mp4" width="3840" height="2160"
             bitrate="15000" delivery="progressive">
    <![CDATA[https://cdn.example.com/ad-4k.mp4]]>
  </MediaFile>
  <!-- Apple TV HD — primary -->
  <MediaFile type="video/mp4" width="1920" height="1080"
             bitrate="5000" delivery="progressive">
    <![CDATA[https://cdn.example.com/ad-1080p.mp4]]>
  </MediaFile>
  <!-- Fallback -->
  <MediaFile type="video/mp4" width="1280" height="720"
             bitrate="2500" delivery="progressive">
    <![CDATA[https://cdn.example.com/ad-720p.mp4]]>
  </MediaFile>
</MediaFiles>

HTTPS and App Transport Security

Apple enforces App Transport Security (ATS) on tvOS. All URLs — media files, VAST tag URLs, impression pixels, tracking events — must use https://. HTTP URLs are blocked at the OS level. Unlike some platforms that silently fail, ATS violations are logged to the device console and typically cause an immediate connection refusal.

Wrapper chains

The IMA SDK for tvOS enforces the same 4-hop wrapper limit as on iOS and Android. Chains longer than 3 hops are risky on living-room networks where latency is less predictable than mobile. Keep chains to 2–3 hops for reliable tvOS delivery.

Skippable ads

Skippable ads are supported. Set skipoffset on <Linear>as a time offset (HH:MM:SS) or percentage. On tvOS, the skip button is rendered by IMA SDK and activated via the Siri Remote click.

<Linear skipoffset="00:00:05">
  <Duration>00:00:30</Duration>
  ...
</Linear>

Duration format

<Duration> must be in HH:MM:SS or HH:MM:SS.mmm format. Bare seconds or ISO 8601 strings are rejected.

→ vastlint rule: VAST-2.0-duration-format

Companion ads

The IMA SDK for tvOS does not render companion ads. Include <CompanionAds> for cross-platform tag compatibility, but companion display metrics will not be reported for tvOS placements.

Pre-flight checklist for Apple tvOS

  • No VPAID — MP4 H.264 + HLS only
  • All URLs use HTTPS (ATS enforced at OS level)
  • Wrapper chain ≤ 3 hops (IMA SDK enforces 4-hop limit; keep shorter for living-room latency)
  • At least one MP4 H.264 <MediaFile> at 720p or higher
  • <Duration> in HH:MM:SS format
  • <Impression> with HTTPS URL present
  • version attribute on root <VAST> element
  • Use OMID <Verification> for measurement, not VPAID
  • Call adsLoader.contentComplete() at end-of-stream for post-roll support
  • Call setNeedsFocusUpdate() on ad break start/end for Siri Remote control
  • Minimum deployment target: tvOS 15

References

Validate your tag against all 118 IAB rules →