Appearance
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):
| Example | Ad source | Ad break behavior |
|---|---|---|
| Manual CSAI | VAST tags from a bundled JSON ad break | The app schedules and plays the ad pod itself |
| Google IMA CSAI | A bundled VMAP | Google IMA requests and sequences client-side ads |
| Google IMA SSAI | A Google DAI VOD stream | Google 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>istrueXorIDVx, 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.
adParameterscomes from the ad (see ResolvingadParameters). 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
onAdFreePodskips the rest of the pod afteronAdCompleted. onUserCancelStreamcloses the player screen.onPopupWebsitepauses the renderer and opensSFSafariViewController, 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.
- The app loads the ad break from
manual_ad_break.jsonand plays content. A time observer detects the midroll at 10 seconds. - 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
adParametersfrom each response - thetruexcompanion first, then<AdParameters>. Fetching at break start gives each break a fresh ad session. - The pod plays ad by ad in the same
AVPlayer. For an Infillion ad that can play interactively, the app startsTruexAdRendererover the player instead of playing the placeholder video. - On
onAdCompletedafteronAdFreePod, the app ends the pod and restores the content. Otherwise, and ononAdErrororonNoAdsAvailable, 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.
- The app requests ads with the bundled VMAP (
ima_csai_vmap.xml). IMA plays the ads in the app's ownAVPlayerthroughIMAAVPlayerVideoDisplay. - On the IMA
STARTEDevent, the app classifies the ad byad.adSystemand checks its position inad.adPodInfo.adPosition. - 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 startsTruexAdRendererwithad.traffickingParameters. Without usabletraffickingParameters, it resumes IMA, which continues the pod. - On
onAdCompletedafteronAdFreePod, the app callsadsManager.discardAdBreak(). Otherwise, and ononAdErrororonNoAdsAvailable, it callsadsManager.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.
- The app requests the DAI VOD stream (content source ID
2496857, video IDtruex-content22-4k) withIMAAVPlayerVideoDisplayon itsAVPlayer. During an ad break, scrubbing is disabled. - On the IMA
STARTEDevent, the app classifies the ad byad.adSystemand its position. For an Infillion ad that can play interactively, it pauses the stream, remembers where the placeholder ends (stream time plusad.duration), and startsTruexAdRenderer. - DAI has no
discardAdBreak, so every skip is a seek. OnonAdCompletedafteronAdFreePod, the app seeks just past the end of the current ad break's cue point. Otherwise, and ononAdErrororonNoAdsAvailable, it seeks to just before the placeholder's end, so the next stitched ad plays. - 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

