Custom Paused Ads
Build custom Pause Ads on iPad with full-screen, transparent WebView, and VAST formats. Configure resume buttons, prefetch ad content, and dismiss ads programmatically.
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: Configurable — from instant display (with prefetching) to any custom duration. A 3–5 second delay is common to filter out accidental pauses.
- Display: A static brand creative fills the screen, or appears as a transparent overlay over the player.
- Resume: Resuming playback automatically dismisses the ad, or use
hidePausedAd()for programmatic dismissal. - Customization: Resume button content, position, and touch-pass-through are fully customizable.
- Frequency: Advertisers can rotate up to 10 creatives per content session.
- Platform: Available on iPad. Calls are no-ops on iPhone.
Prerequisites
Before using Pause Ads, ensure you have:
- Completed the iOS Integration Guide
- 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")The SDK also requires a displayDelegate on managedAdManager to present the ad. This is configured automatically when you call StreamLayer.createOverlay(...).
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 present the ad. The example below schedules the ad after 5 seconds.
Use a DispatchWorkItem to defer the call to showPausedAd(_:) and cancel it when playback resumes. If the ad has already been presented before the user resumes, dismiss it with hidePausedAd().
private var pausedAdWorkItem: DispatchWorkItem?
func handlePlayingState() {
pausedAdWorkItem?.cancel()
pausedAdWorkItem = nil
StreamLayer.hidePausedAd()
}
func handlePausedState() {
let workItem = DispatchWorkItem { [weak self] in
guard self?.pausedAdWorkItem?.isCancelled == false else { return }
self?.presentPausedAd()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: workItem)
self.pausedAdWorkItem = workItem
}
private func presentPausedAd() {
// Pick whichever pause-ad type fits your inventory — see "Displaying a Custom Pause Ad" below
let vastURL = URL(string: "https://example.com/vast.xml")!
StreamLayer.showPausedAd(.transparentBackgroundVAST(
vastTagURL: vastURL,
resumeButton: .hidden
))
}Displaying a Custom Pause Ad
Call StreamLayer.showPausedAd(_:) to present a promotional StreamLayer Element when the user pauses video playback. The Element includes a resume button that dismisses the ad and continues playback.
The adType parameter selects one of three formats:
.fullScreen(configuration:)— A full-bleed ad with customizable title, body, sponsor logo, and background image..transparentBackground(url:slot:resumeButton:bypassTouches:)— A transparent overlay rendered in aWKWebView. Passurlfor a direct creative URL, orslotto let the bell-ad proxy build the URL from an ad-server slot identifier..transparentBackgroundVAST(vastTagURL:resumeButton:bypassTouches:)— A transparent overlay that parses a VAST tag, renders the NonLinear / Companion static image, fires<Impression>pixels on display, and fires<NonLinearClickTracking>pixels on user tap.
Important:
- Set the
displayDelegateonmanagedAdManagerbefore calling this method. The delegate is configured automatically when you callcreateOverlay().- 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 — see the tvOS Custom Paused Ads guide.
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")
)
StreamLayer.showPausedAd(.fullScreen(configuration: config))
}You can also hydrate a full-screen ad from a VAST tag — pass the tag URL via SLRPausedAdConfiguration.vastTagURL and the SDK will parse it and apply the extracted creative on top of your configuration.
Transparent WebView Pause Ad
Use .transparentBackground for HTML creative loaded in a WKWebView. Provide either a direct creative url or a slot identifier resolved by the SDK's bell-ad proxy.
func handlePausedState() {
// Direct creative URL
let adURL = URL(string: "https://example.com/pause-ad.html")!
StreamLayer.showPausedAd(.transparentBackground(
url: adURL,
resumeButton: .hidden,
bypassTouches: false
))
// Or let the bell-ad proxy resolve an ad-server slot
StreamLayer.showPausedAd(.transparentBackground(
slot: "/12345/pause_ad_ctv",
resumeButton: .hidden
))
}Pass exactly one of
urlorslot. Theslotform opts the request into the SDK's bell-ad proxy, which handles ad-server resolution and analytics for you.
Transparent VAST Pause Ad
Use .transparentBackgroundVAST when your ad server returns a VAST 4.x tag with a NonLinear or Companion static image. The SDK:
- Fetches and parses the VAST XML.
- Downloads the static image referenced by the NonLinear / Companion creative.
- Renders the image as a transparent overlay over the player.
- Fires the VAST
<Impression>pixels once at render time (per the VAST 4.x spec — impressions count when the ad is visible, not when the tag is parsed). - On user tap, fires the
<NonLinearClickTracking>pixels and opens the click-through URL in the system browser.
func handlePausedState() {
let vastURL = URL(string: "https://example.com/vast.xml")!
StreamLayer.showPausedAd(.transparentBackgroundVAST(
vastTagURL: vastURL,
resumeButton: .hidden,
bypassTouches: false
))
}If the VAST response contains no usable static image, the SDK logs an error and skips presentation — playback is not interrupted.
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
let vastURL = URL(string: "https://example.com/vast.xml")!
// Default position with custom image
let config = SLRResumeButtonConfiguration.image(
UIImage(named: "play_button")!,
size: CGSize(width: 100, height: 100)
)
StreamLayer.showPausedAd(.transparentBackgroundVAST(vastTagURL: vastURL, resumeButton: config))
// Default position with text
let textConfig = SLRResumeButtonConfiguration.text("RESUME")
StreamLayer.showPausedAd(.transparentBackgroundVAST(vastTagURL: vastURL, resumeButton: textConfig))
// Hidden button
StreamLayer.showPausedAd(.transparentBackgroundVAST(vastTagURL: vastURL, resumeButton: .hidden))The same configuration applies to .transparentBackground(url:) and .transparentBackground(slot:) — pass the resume button via the resumeButton: argument.
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(.transparentBackgroundVAST(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(.transparentBackgroundVAST(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(.transparentBackgroundVAST(vastTagURL: vastURL, resumeButton: bottomRightConfig))
// Center of screen
let centerConfig = SLRResumeButtonConfiguration.text(
"Resume Video",
position: .center
)
StreamLayer.showPausedAd(.transparentBackgroundVAST(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(.transparentBackgroundVAST(vastTagURL: vastURL, resumeButton: customConfig))Platform-Specific Defaults
The SDK provides orientation-aware default positions for iPad:
iOS (iPad):
.defaultLandscape— Bottom-left: (60pt left, 64pt from bottom).defaultPortrait— Bottom-left: (16pt left, 16pt 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:
iOS:
- Button is completely hidden
- Background tap gesture is enabled (tap anywhere outside the ad surface to resume)
tvOS:
- Button is completely hidden
- 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(.transparentBackgroundVAST(vastTagURL: vastURL, resumeButton: .hidden))bypassTouches Behavior
bypassTouches BehaviorBoth .transparentBackground and .transparentBackgroundVAST accept a bypassTouches: Bool argument:
false(default) — The overlay absorbs all touches. Taps anywhere route to the ad surface and resume button.true— Taps that miss the ad surface pass through to the host UI underneath (for example, the video player controls). Taps on the ad surface and resume button still work as expected.
// Allow the host app's player controls to remain interactive while the ad is shown
StreamLayer.showPausedAd(.transparentBackgroundVAST(
vastTagURL: vastURL,
resumeButton: .hidden,
bypassTouches: true
))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
)Prefetch behavior depends on the ad type:
.fullScreen(configuration:)— Prefetches the configuration's images, plus any image extracted from the optionalvastTagURL..transparentBackground(url:slot:...)— No-op. TheWKWebViewhandles its own loading; the completion fires immediately with.success(())..transparentBackgroundVAST(vastTagURL:...)— Parses the VAST tag and pre-downloads the NonLinear / Companion static image. Impression and click-tracking URLs are cached but not yet fired (they fire on display / tap).
Examples
// Prefetch a VAST pause ad with default 1-hour expiration
let vastURL = URL(string: "https://example.com/vast.xml")!
StreamLayer.prefetchPausedAd(.transparentBackgroundVAST(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(
.transparentBackgroundVAST(vastTagURL: vastURL),
expirationInterval: 1800
) { result in
print("Ad cached for 30 minutes")
}
// Prefetch with no expiration (cache until manually cleared)
StreamLayer.prefetchPausedAd(
.transparentBackgroundVAST(vastTagURL: vastURL),
expirationInterval: .infinity
) { result in
print("Ad cached indefinitely")
}
// Prefetch fullscreen ad with configuration (and optional VAST hydration)
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 func playVideo(_ userInitiated: Bool) method on SLROverlayDelegate.
Implementation Example
extension YourViewController: SLROverlayDelegate {
func playVideo(_ userInitiated: Bool) {
// Resume video playback
player.play()
// Cancel any pending ad display
pausedAdWorkItem?.cancel()
pausedAdWorkItem = nil
}
}The userInitiated parameter indicates whether the action was triggered by the user (true) or programmatically (false), allowing you to handle analytics or logging accordingly.
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.
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()
}
}Related
- Pause Ads — overview — Cross-platform overview of the Pause Ads format
- iOS Integration Guide — SDK installation, initialization, and overlay setup
- iOS API Reference — Reference for
SLRPausedAdType,SLRPausedAdConfiguration, andSLRResumeButtonConfiguration - SLROverlayDelegate — Delegate methods invoked by the SDK to control playback
- Custom Paused Ads (tvOS) — tvOS equivalent guide
Updated 4 days ago
