Custom Paused Ads

What are Pause Ads

Pause Ads display brand messages during natural breaks in playback. When a viewer pauses live or on-demand content, an ad appears on-screen after a brief delay. This format provides a high-visibility, static canvas that integrates naturally with the viewing experience. Pause Ads are effective for driving awareness, shoppability, and branded interactions.

Key Characteristics

  • Trigger: The viewer pauses live or on-demand content.
  • Delay: The ad appears after approximately 5 seconds.
  • Display: A static brand creative fills the screen or appears in a sidebar overlay.
  • Resume: Resuming playback automatically dismisses the ad, or use hidePausedAd() for programmatic dismissal.
  • Customization: Resume button position, content, and styling are fully customizable.
  • Frequency: Advertisers can rotate up to 10 creatives per content session.

Prerequisites

Before using Pause Ads, ensure you have:

  1. Completed the tvOS Integration Guide
  2. Called StreamLayer.createSession(for: "<event_id>") to establish an active session

The event session must be created before displaying pause ads, as it provides necessary context for analytics tracking.

// Create a session for the event
StreamLayer.createSession(for: "your-event-id")

Delay Pause Ads

Delay displaying pause ads to avoid brief or accidental pauses. Typically show a pause ad 3–5 seconds after the user pauses playback. If playback doesn't resume within this window, treat the pause as intentional and start an ad break. The example below schedules the ad after 5 seconds.

Use a DispatchWorkItem to defer the ad break and cancel it when playback resumes.

private var adBreakWorkItem: DispatchWorkItem?

func handlePlayingState() {
    StreamLayer.stopADBreak()
    adBreakWorkItem?.cancel()
    adBreakWorkItem = nil
}

func handlePausedState() {
    let adBreakWorkItem = DispatchWorkItem { [weak self] in
        guard self?.adBreakWorkItem?.isCancelled == false else { return }
        StreamLayer.startADBreak()
    }
    DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: adBreakWorkItem)
    self.adBreakWorkItem = adBreakWorkItem
}

Displaying a Custom Pause Ad

Call StreamLayer.showPausedAd(_:) to present a fullscreen promotional overlay when the user pauses video playback. The overlay includes a resume button that dismisses the ad and continues playback.

The adType parameter determines the ad format:

  • .fullScreen(configuration:) — A full-bleed ad with customizable title, body, sponsor logo, and background image.
  • .transparentBackground(vastTagURL:, resumeButton:) — A transparent background ad with content loaded from a VAST tag URL.

Important:

  • Set the displayDelegate on managedAdManager before calling this method. The delegate is configured automatically when you call createOverlay().
  • iOS SDK: Pause Ads are only available on iPad. Calling this method on iPhone will have no effect.
  • tvOS SDK: Pause Ads are fully supported on tvOS.

Full Screen Pause Ad

func handlePausedState() {
    // Show a fullscreen pause ad with custom configuration
    let config = SLRPausedAdConfiguration(
        title: "Sponsored Content",
        body: "Check out our latest offers",
        sponsorLogo: UIImage(named: "sponsor_logo"),
        backgroundImage: UIImage(named: "ad_background"),
        vastTagURL: nil
    )
    StreamLayer.showPausedAd(.fullScreen(configuration: config))
}

Transparent Background Pause Ad

func handlePausedState() {
    // Show a transparent background ad with VAST tag URL
    let vastURL = URL(string: "https://example.com/vast.xml")!
    let resumeButtonConfig: SLRResumeButtonConfiguration? = nil // See configuration options below
    StreamLayer.showPausedAd(.transparentBackground(vastTagURL: vastURL, resumeButton: resumeButtonConfig))
}

Resume Button Configuration

The resume button can be fully customized for transparent background pause ads using SLRResumeButtonConfiguration. You can control both the button's content (image, text, or hidden) and its position on screen.

API Structure

/// Configuration for the resume button displayed on transparent background paused ads
public struct SLRResumeButtonConfiguration {
    /// Button content type
    public enum Content {
        /// Display resume button with a custom image and size
        case image(UIImage, size: CGSize)

        /// Display resume button with custom text
        case text(String)

        /// Hide the resume button completely
        case hidden
    }

    /// Predefined positions for the resume button
    public enum Position {
        case topLeft(insets: UIEdgeInsets)
        case topRight(insets: UIEdgeInsets)
        case bottomLeft(insets: UIEdgeInsets)
        case bottomRight(insets: UIEdgeInsets)
        case center
        case custom((UIView, UIView) -> Void) // Custom closure for complete control

        /// Default position for landscape iPad
        public static var defaultLandscape: Position

        /// Default position for portrait iPad/iPhone
        public static var defaultPortrait: Position
    }

    public let content: Content
    public let position: Position
}

Basic Usage

// Default position with custom image
let config = SLRResumeButtonConfiguration.image(
    UIImage(named: "play_button")!,
    size: CGSize(width: 100, height: 100)
)
StreamLayer.showPausedAd(.transparentBackground(vastTagURL: vastURL, resumeButton: config))

// Default position with text
let textConfig = SLRResumeButtonConfiguration.text("RESUME")
StreamLayer.showPausedAd(.transparentBackground(vastTagURL: vastURL, resumeButton: textConfig))

// Hidden button
StreamLayer.showPausedAd(.transparentBackground(vastTagURL: vastURL, resumeButton: .hidden))

Predefined Positions

Position the button at common screen locations with customizable insets:

// Top-left corner
let topLeftConfig = SLRResumeButtonConfiguration(
    content: .text("RESUME"),
    position: .topLeft(insets: UIEdgeInsets(top: 60, left: 80, bottom: 0, right: 0))
)
StreamLayer.showPausedAd(.transparentBackground(vastTagURL: vastURL, resumeButton: topLeftConfig))

// Top-right corner
let topRightConfig = SLRResumeButtonConfiguration.image(
    UIImage(systemName: "play.circle.fill")!,
    size: CGSize(width: 80, height: 80),
    position: .topRight(insets: UIEdgeInsets(top: 60, left: 0, bottom: 0, right: 80))
)
StreamLayer.showPausedAd(.transparentBackground(vastTagURL: vastURL, resumeButton: topRightConfig))

// Bottom-right corner
let bottomRightConfig = SLRResumeButtonConfiguration(
    content: .text("Continue"),
    position: .bottomRight(insets: UIEdgeInsets(top: 0, left: 0, bottom: 80, right: 80))
)
StreamLayer.showPausedAd(.transparentBackground(vastTagURL: vastURL, resumeButton: bottomRightConfig))

// Center of screen
let centerConfig = SLRResumeButtonConfiguration.text(
    "Resume Video",
    position: .center
)
StreamLayer.showPausedAd(.transparentBackground(vastTagURL: vastURL, resumeButton: centerConfig))

Custom Positioning with Closure

For complete control over button placement and styling, use the .custom position with a closure:

let customConfig = SLRResumeButtonConfiguration(
    content: .image(playIcon, size: CGSize(width: 100, height: 100)),
    position: .custom { button, containerView in
        // Apply SnapKit constraints
        button.snp.makeConstraints { make in
            make.trailing.equalToSuperview().offset(-150)
            make.centerY.equalToSuperview().offset(50)
            make.width.height.equalTo(100)
        }

        // Optional: Add custom styling
        button.layer.cornerRadius = 50
        button.layer.shadowColor = UIColor.black.cgColor
        button.layer.shadowOpacity = 0.3
        button.layer.shadowOffset = CGSize(width: 0, height: 4)
        button.layer.shadowRadius = 8
        button.backgroundColor = UIColor.white.withAlphaComponent(0.2)
    }
)
StreamLayer.showPausedAd(.transparentBackground(vastTagURL: vastURL, resumeButton: customConfig))

Platform-Specific Defaults

The SDK provides platform-aware default positions:

tvOS:

  • .defaultTVOS — Bottom-left: (60pt left, 64pt from bottom)

These defaults are automatically applied when you don't specify a position parameter.

Hidden Button Behavior

The .hidden configuration hides the resume button:

tvOS:

  • Button becomes transparent
  • Remains in view hierarchy for focus handling
  • Still responds to interaction when focused
// Hide the button (user can tap anywhere to resume on iOS)
StreamLayer.showPausedAd(.transparentBackground(vastTagURL: vastURL, resumeButton: .hidden))

Prefetching Pause Ads

For improved user experience, prefetch pause ads before displaying them. This method downloads and caches VAST tag content and associated images, ensuring smooth presentation when the user pauses playback.

/// Prefetches a paused ad by parsing the VAST tag URL and downloading any associated images.
///
/// The prefetching process includes:
/// - Parsing the VAST tag XML
/// - Downloading and caching any images referenced in the VAST response
/// - Validating the ad content
/// - Caching with automatic expiration after the specified interval
///
/// - Parameters:
///   - adType: The type of paused ad to prefetch
///   - expirationInterval: Time in seconds after which the cached ad expires (default: 3600 = 1 hour)
///   - completion: A completion handler called when prefetching completes
public class func prefetchPausedAd(
    _ adType: SLRPausedAdType,
    expirationInterval: TimeInterval = 3600,
    completion: @escaping (Swift.Result<Void, Error>) -> Void
)

Examples

// Prefetch with default 1-hour expiration
let vastURL = URL(string: "https://example.com/vast.xml")!
StreamLayer.prefetchPausedAd(.transparentBackground(vastTagURL: vastURL)) { result in
    switch result {
    case .success:
        print("Ad cached for 1 hour")
    case .failure(let error):
        print("Prefetch failed: \(error)")
    }
}

// Prefetch with custom 30-minute expiration
StreamLayer.prefetchPausedAd(
    .transparentBackground(vastTagURL: vastURL),
    expirationInterval: 1800
) { result in
    print("Ad cached for 30 minutes")
}

// Prefetch with no expiration (cache until manually cleared)
StreamLayer.prefetchPausedAd(
    .transparentBackground(vastTagURL: vastURL),
    expirationInterval: .infinity
) { result in
    print("Ad cached indefinitely")
}

// Prefetch fullscreen ad with configuration
let config = SLRPausedAdConfiguration(
    title: "Sponsored Content",
    body: "Check out our latest offers",
    sponsorLogo: UIImage(named: "sponsor_logo"),
    backgroundImage: UIImage(named: "ad_background"),
    vastTagURL: URL(string: "https://example.com/vast.xml")
)
StreamLayer.prefetchPausedAd(.fullScreen(configuration: config)) { result in
    switch result {
    case .success:
        print("Fullscreen ad prefetched successfully")
    case .failure(let error):
        print("Prefetch failed: \(error)")
    }
}

Note: When a prefetched ad is presented via showPausedAd(_:), the cache is automatically cleared after the ad is displayed. This ensures fresh content on subsequent pauses.

Clearing Prefetched Ads

Use this method to free up memory by removing cached ad content. After calling this method, subsequent calls to showPausedAd(_:) will need to re-fetch and parse VAST tags.

/// Clears all prefetched paused ad data from memory.
///
/// Use this method to free up memory by removing cached ad content.
///
/// - Note: This only clears the in-memory cache. The underlying image cache managed by
///   the image loading system is not affected.
public class func clearPrefetchedPausedAds()

Example

// Clear prefetched ads when the user navigates away or to free up memory
StreamLayer.clearPrefetchedPausedAds()

Note: The cache is automatically cleared when a prefetched ad is presented, so manual clearing is typically only needed when:

  • The user exits the viewing session
  • You need to free up memory
  • You want to force fresh content to be fetched

Dismissal

User-Initiated Dismissal

The user dismisses the pause ad by tapping the resume button. This action triggers the didResumePlaying() method on StreamLayerTVOSDelegate.

Implementation Example

extension YourViewController: StreamLayerTVOSDelegate {
    func didResumePlaying() {
        // Resume video playback
        player.play()

        // Stop the ad break
        StreamLayer.stopADBreak()

        // Cancel any pending ad display
        adBreakWorkItem?.cancel()
        adBreakWorkItem = nil
    }
}

Programmatic Dismissal

You can manually dismiss a currently displayed pause ad using StreamLayer.hidePausedAd(). This is useful when you need to dismiss the ad based on custom logic or external events.

/// Manually dismisses the currently displayed paused ad.
///
/// Use this method to programmatically hide a paused ad that is currently being shown.
/// This is useful when you need to dismiss the ad based on custom logic or user actions
/// outside of the standard resume/play button.
///
/// - Note: This method has no effect if no paused ad is currently displayed.
/// - Note: iOS SDK - This feature is only available on iPad. Calling this method on iPhone will have no effect.
/// - Note: tvOS SDK - This feature is fully supported on tvOS.
public class func hidePausedAd()

Example

// Dismiss the ad when receiving a specific notification
NotificationCenter.default.addObserver(forName: .userDidPerformAction, object: nil, queue: .main) { _ in
    StreamLayer.hidePausedAd()
}

// Dismiss the ad after a timeout
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
    StreamLayer.hidePausedAd()
}

// Dismiss the ad based on custom business logic
func handleCustomEvent() {
    if shouldDismissAd {
        StreamLayer.hidePausedAd()
        player.play()
    }
}