Skip to content

Examples ​

The infillion-ads-ios-examples repository contains two iPhone and iPad apps with the same three complete integrations of TrueX and IDVx ads into an AVPlayer app - one in Swift (swift-ios-app) and one in Objective-C (objc-ios-app):

ExampleAd sourceAd break behavior
Manual CSAIVAST tags from a bundled JSON ad breakThe app schedules and plays the ad pod itself
Google IMA CSAIA bundled VMAPGoogle IMA requests and sequences client-side ads
Google IMA SSAIA Google DAI VOD streamGoogle DAI returns one stream with stitched ad breaks

Each example has its own player, ad, and TruexAdRenderer code on purpose, so you can pick the one closest to your setup and read it from top to bottom. Both apps have the same examples, flows, and sample configuration; only the language differs. The Swift app creates the renderer through the small Objective-C TruexRendererFactory - see Using TruexAdOptions from Swift.

Running the Apps ​

Requirements:

  • Xcode 16 or later
  • An iOS simulator or device running iOS 15.6 or later

Open swift-ios-app/InfillionAdsExamples.xcodeproj (Swift) or objc-ios-app/InfillionAdsExamplesObjC.xcodeproj (Objective-C). Xcode resolves the Google IMA SDK and TruexAdRenderer-iOS through Swift Package Manager. Run the InfillionAdsExamples or InfillionAdsExamplesObjC scheme on an iPhone simulator and pick an example. The two apps have different bundle IDs, so both can be installed side by side.

Each example shows its current state (content, linear ad, interactive ad, errors) in a status panel, and logs it with the [InfillionAdsExamples] prefix.

Test IP parameter

The sample VAST tags include &ip=158.106.195.210, the IP of the Infillion NYC office. The TrueX ad server serves ads in the US and Canada only, and this parameter lets teams and CI runners outside those regions get filled test ads. Do not use it in production.

Shared Rules ​

All three examples follow the same rules, implemented as small functions you can copy:

  • An ad is an Infillion ad if its <AdSystem> is trueX or IDVx, compared case-insensitively.
  • TrueX is interactive only as the first ad in the pod; at a later position it plays as a regular linear ad. IDVx is interactive in any position.
  • adParameters comes from the ad (see Resolving adParameters). If it is missing or invalid, or the renderer can't be created, the renderer is not started and the pod continues.
  • Only a TrueX ad that called onAdFreePod skips the rest of the pod after onAdCompleted.
  • onUserCancelStream closes the player screen. onPopupWebsite pauses the renderer and opens SFSafariViewController, and the renderer resumes when Safari closes.
swift
/// TrueX is interactive only as the first ad in a pod; IDVx in any position.
func canPlayImaCsaiInteractive(_ type: ImaCsaiAdType, adPosition: Int) -> Bool {
    switch type {
    case .truex:
        return adPosition == 1
    case .idvx:
        return true
    case .linear:
        return false
    }
}

/// Only a TrueX ad that earned credit (onAdFreePod) skips the rest of the pod.
func shouldDiscardImaCsaiAdBreak(_ type: ImaCsaiAdType, earnedCredit: Bool) -> Bool {
    type == .truex && earnedCredit
}
objective-c
// TrueX is interactive only as the first ad in a pod; IDVx in any position.
FOUNDATION_EXTERN BOOL CanPlayImaCsaiInteractive(ImaCsaiAdType type, NSInteger adPosition);

// Only a TrueX ad that earned credit (onAdFreePod) skips the rest of the pod.
FOUNDATION_EXTERN BOOL ShouldDiscardImaCsaiAdBreak(ImaCsaiAdType type, BOOL earnedCredit);

The Objective-C app has the same rules as C functions with capitalized names (ClassifyImaCsaiAd, CanPlayImaCsaiInteractive, ImaSsaiAdParameters, ...), covered by the same unit tests.

Manual CSAI ​

Use this example when your app or your own ad layer decides when an ad break starts and which ads it contains.

  1. The app loads the ad break from manual_ad_break.json and plays content. A time observer detects the midroll at 10 seconds.
  2. At the midroll, the app saves the content position, pauses content, and fetches the VAST tag of every Infillion ad in the pod. It resolves adParameters from each response - the truex companion first, then <AdParameters>. Fetching at break start gives each break a fresh ad session.
  3. The pod plays ad by ad in the same AVPlayer. For an Infillion ad that can play interactively, the app starts TruexAdRenderer over the player instead of playing the placeholder video.
  4. On onAdCompleted after onAdFreePod, the app ends the pod and restores the content. Otherwise, and on onAdError or onNoAdsAvailable, it plays the next ad.

The sample fixture uses the companion tag for TrueX and the generic tag for IDVx, so one ad break exercises both formats.

swift
private func playAd(at index: Int) {
    guard let adBreak, index < adBreak.ads.count else {
        endAdBreak()
        return
    }
    currentAdIndex = index
    let ad = adBreak.ads[index]
    let position = index + 1
    if canPlayInteractive(ad.type, adPosition: position) {
        if let adParameters = adParametersByIndex[index] {
            startInteractiveAd(ad, adParameters: adParameters, position: position)
        } else {
            // No usable adParameters: don't start the renderer, continue the pod.
            playAd(at: index + 1)
        }
    } else {
        playLinearAd(ad, position: position)
    }
}

private func finishInteractiveAd(skipRemainingAds: Bool) {
    disposeRenderer()
    if skipRemainingAds {
        // TrueX credit earned: skip the rest of the pod
        endAdBreak()
    } else {
        playAd(at: currentAdIndex + 1)
    }
}
objective-c
- (void)playAdAtIndex:(NSInteger)index {
    if (index >= (NSInteger)_adBreak.ads.count) {
        [self endAdBreak];
        return;
    }
    _currentAdIndex = index;
    ManualAd *ad = _adBreak.ads[(NSUInteger)index];
    NSInteger position = index + 1;
    if (CanPlayInteractive(ad.type, position)) {
        NSDictionary *adParameters = _adParametersByIndex[@(index)];
        if (adParameters) {
            [self startInteractiveAd:ad adParameters:adParameters position:position];
        } else {
            // No usable adParameters: don't start the renderer, continue the pod.
            [self playAdAtIndex:index + 1];
        }
    } else {
        [self playLinearAd:ad position:position];
    }
}

- (void)finishInteractiveAdSkippingRemainingAds:(BOOL)skipRemainingAds {
    [self disposeRenderer];
    if (skipRemainingAds) {
        // TrueX credit earned: skip the rest of the pod
        [self endAdBreak];
    } else {
        [self playAdAtIndex:_currentAdIndex + 1];
    }
}

Files: ManualCsaiViewController.swift / .m, ManualVastPayload.swift / .m, ManualAdBreak.swift / .m - guide: manual-csai.md

Google IMA CSAI ​

Use this example when Google IMA requests and plays your client-side ads.

  1. The app requests ads with the bundled VMAP (ima_csai_vmap.xml). IMA plays the ads in the app's own AVPlayer through IMAAVPlayerVideoDisplay.
  2. On the IMA STARTED event, the app classifies the ad by ad.adSystem and checks its position in ad.adPodInfo.adPosition.
  3. For an Infillion ad that can play interactively, the app pauses the IMAAdsManager, seeks the player to just before the end of the placeholder, and starts TruexAdRenderer with ad.traffickingParameters. Without usable traffickingParameters, it resumes IMA, which continues the pod.
  4. On onAdCompleted after onAdFreePod, the app calls adsManager.discardAdBreak(). Otherwise, and on onAdError or onNoAdsAvailable, it calls adsManager.resume(): the placeholder finishes right away and IMA plays the next ad.

Because the placeholder plays in the app's AVPlayer, moving past it is a plain AVPlayer seek.

swift
private func handleAdStarted(_ ad: IMAAd) {
    let type = classifyImaCsaiAd(adSystem: ad.adSystem)
    let position = ad.adPodInfo.adPosition
    guard canPlayImaCsaiInteractive(type, adPosition: position) else {
        return
    }
    // Pause IMA and move the placeholder to its end, so it completes as soon as IMA resumes.
    adsManager?.pause()
    skipPlaceholderAd()
    guard let adParameters = imaCsaiAdParameters(traffickingParameters: ad.traffickingParameters) else {
        // No usable adParameters: don't start the renderer, continue the pod.
        adsManager?.resume()
        return
    }
    currentInteractiveType = type
    truexAdCreditReceived = false
    guard let renderer = TruexRendererFactory.renderer(withAdParameters: adParameters, delegate: self) else {
        // No renderer means no delegate events: continue the pod.
        finishInteractiveAd(skipRemainingAds: false)
        return
    }
    truexAdRenderer = renderer
    renderer.start(view)
}

private func finishInteractiveAd(skipRemainingAds: Bool) {
    disposeRenderer()
    if skipRemainingAds {
        adsManager?.discardAdBreak()
    } else {
        adsManager?.resume()
    }
}
objective-c
- (void)handleAdStarted:(IMAAd *)ad {
    ImaCsaiAdType type = ClassifyImaCsaiAd(ad.adSystem);
    NSInteger position = ad.adPodInfo.adPosition;
    if (!CanPlayImaCsaiInteractive(type, position)) {
        return;
    }
    // Pause IMA and move the placeholder to its end, so it completes as soon as IMA resumes.
    [_adsManager pause];
    [self skipPlaceholderAd];
    NSDictionary *adParameters = ImaCsaiAdParameters(ad.traffickingParameters);
    if (!adParameters) {
        // No usable adParameters: don't start the renderer, continue the pod.
        [_adsManager resume];
        return;
    }
    _currentInteractiveType = type;
    _truexAdCreditReceived = NO;
    TruexAdOptions options = DefaultOptions();
    options.supportsUserCancelStream = YES;
#if DEBUG
    options.enableWebViewDebugging = YES;
#endif
    TruexAdRenderer *renderer = [[TruexAdRenderer alloc] initWithAdParameters:adParameters
                                                                      options:options
                                                                     delegate:self];
    if (!renderer) {
        // No renderer means no delegate events: continue the pod.
        [self finishInteractiveAdSkippingRemainingAds:NO];
        return;
    }
    _truexAdRenderer = renderer;
    [renderer start:self.view];
}

- (void)finishInteractiveAdSkippingRemainingAds:(BOOL)skipRemainingAds {
    [self disposeRenderer];
    if (skipRemainingAds) {
        [_adsManager discardAdBreak];
    } else {
        [_adsManager resume];
    }
}

onAdCompleted calls finishInteractiveAd with shouldDiscardImaCsaiAdBreak(currentInteractiveType, earnedCredit: truexAdCreditReceived); onAdError and onNoAdsAvailable call it with false.

Files: ImaCsaiViewController.swift / .m, ImaCsaiAdPayload.swift / .m - guide: ima-csai.md

Google IMA SSAI ​

Use this example when Google IMA DAI returns one stream with your ads stitched in.

  1. The app requests the DAI VOD stream (content source ID 2496857, video ID truex-content22-4k) with IMAAVPlayerVideoDisplay on its AVPlayer. During an ad break, scrubbing is disabled.
  2. On the IMA STARTED event, the app classifies the ad by ad.adSystem and its position. For an Infillion ad that can play interactively, it pauses the stream, remembers where the placeholder ends (stream time plus ad.duration), and starts TruexAdRenderer.
  3. DAI has no discardAdBreak, so every skip is a seek. On onAdCompleted after onAdFreePod, the app seeks just past the end of the current ad break's cue point. Otherwise, and on onAdError or onNoAdsAvailable, it seeks to just before the placeholder's end, so the next stitched ad plays.
  4. A time observer seeks over ad breaks that were already played.
swift
/// Stream time just before the placeholder ad ends, so the stitched stream moves on to the next ad.
func imaSsaiPlaceholderEndTime(adStartStreamTime: Double, adDuration: Double) -> Double {
    max(0, adStartStreamTime + adDuration - 0.1)
}

/// Stream time just past the current ad break, so the rest of the break is skipped.
func imaSsaiAdBreakSkipTime(adBreakEndStreamTime: Double) -> Double {
    adBreakEndStreamTime + 0.1
}

private func finishInteractiveAd(skipAdBreak: Bool) {
    disposeRenderer()
    let streamTime = currentStreamTime
    if skipAdBreak,
        let cuepoint = (streamManager?.cuepoints as? [IMACuepoint])?.first(where: {
            $0.startTime <= streamTime && streamTime < $0.endTime
        })
    {
        // TrueX credit earned: seek past the whole stitched ad break
        seekStream(to: imaSsaiAdBreakSkipTime(adBreakEndStreamTime: cuepoint.endTime))
    } else {
        // Opt-out, error, or IDVx: seek to the placeholder end, the next stitched ad plays
        seekStream(to: placeholderEndTime)
    }
    player.play()
}
objective-c
// Stream time just before the placeholder ad ends, so the stitched stream moves on to the next ad.
FOUNDATION_EXTERN double ImaSsaiPlaceholderEndTime(double adStartStreamTime, double adDuration);

// Stream time just past the current ad break, so the rest of the break is skipped.
FOUNDATION_EXTERN double ImaSsaiAdBreakSkipTime(double adBreakEndStreamTime);

- (void)finishInteractiveAdSkippingAdBreak:(BOOL)skipAdBreak {
    [self disposeRenderer];
    IMACuepoint *cuepoint = skipAdBreak ? [self cuepointAt:self.currentStreamTime playedOnly:NO] : nil;
    if (cuepoint) {
        // TrueX credit earned: seek past the whole stitched ad break
        [self seekStreamTo:ImaSsaiAdBreakSkipTime(cuepoint.endTime)];
    } else {
        // Opt-out, error, or IDVx: seek to the placeholder end, the next stitched ad plays
        [self seekStreamTo:_placeholderEndTime];
    }
    [_player play];
}

Demo-only `adParameters`

In a production DAI integration, read adParameters from the ad: imaSsaiAdParameters(traffickingParameters: ad.traffickingParameters) in Swift, ImaSsaiAdParameters(ad.traffickingParameters) in Objective-C. The sample app instead loads them from the iOS sample tags in ImaSsaiDemoAdParameters, because the sample DAI stream can't be changed. Delete that file when you copy the example.

Files: ImaSsaiViewController.swift / .m, ImaSsaiAdPayload.swift / .m - guide: ima-ssai.md

Integration documentation for Infillion Ads