Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Fixed

- Conviva
- Fixed an issue where a THEOads ad break that failed before any ad was available, for example on an empty VAST response, was not reported as an ad attempt.

## [11.0.4] - 2026-05-29

### Changed
Expand Down
1 change: 1 addition & 0 deletions Code/Conviva/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ To support custom feature builds of THEOplayerSDK perform the following steps:

1. Create a Podfile if you don't already have one. From the root of your project directory, run the following command: `pod init`
2. To your Podfile, add the Conviva connector pods that you want to use in your app: `pod 'THEOplayer-Connector-Conviva'`
- When using THEOads, also add the THEOads subspec so that failed THEOads ad breaks (e.g. an empty VAST response) are reported to Conviva: `pod 'THEOplayer-Connector-Conviva/THEOads'`
3. Install the pods using `pod install` , then open your `.xcworkspace` file to see the project in Xcode.

To support custom feature builds of THEOplayerSDK perform the following steps:
Expand Down
9 changes: 9 additions & 0 deletions Code/Conviva/Source/Base/ConvivaConnector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public class ConvivaConnector {
private let theoliveForwarder: THEOliveEventForwarder
private let theoliveHandler: THEOliveHandler
#endif

#if canImport(THEOplayerTHEOadsIntegration)
private let theoadsForwarder: THEOadsEventForwarder
#endif

public convenience init?(
configuration: ConvivaConfiguration,
Expand Down Expand Up @@ -51,6 +55,11 @@ public class ConvivaConnector {
self.theoliveHandler = THEOliveHandler(endpoints: self.endPoints, storage: self.storage)
self.theoliveForwarder = THEOliveEventForwarder(player: player, handler: self.theoliveHandler)
#endif

#if canImport(THEOplayerTHEOadsIntegration)
// THEOads level handling
self.theoadsForwarder = THEOadsEventForwarder(player: player, handler: self.adHandler)
#endif
}

public func destroy() {
Expand Down
76 changes: 60 additions & 16 deletions Code/Conviva/Source/Base/Events/ConvivaHandlers/AdHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,37 @@ import THEOplayerSDK

class AdHandler {
static let serializationFormatter: NumberFormatter = createSerializationFormatter()
/// The ad technology reported to Conviva for THEOads (SGAI).
/// SGAI isn't officially supported by Conviva yet, so we report it with our own string for now.
static let sgaiAdTechnology = "Server Guided"
private weak var endpoints: ConvivaEndpoints?
private weak var storage: ConvivaStorage?

private var isAdBreakActive: Bool = false
private var adBreakCounter: Int = 0

init(endpoints: ConvivaEndpoints, storage: ConvivaStorage) {
self.endpoints = endpoints
self.storage = storage
}

func setAdInfo(_ adInfo: [String: Any]) {
log("adAnalytics.setAdInfo: \(adInfo)")
self.endpoints?.adAnalytics.setAdInfo(adInfo)
}


/// Ad metadata shared between successful and failed ad reporting: the tags needed to attach
/// the ad session to the content session (`c3.csid` and `contentAssetName`).
private func collectBaseAdMetadata() -> [String: Any] {
var info: [String: Any] = [:]
if let contentAssetName = self.storage?.metadataEntryForKey(CIS_SSDK_METADATA_ASSET_NAME) {
info["contentAssetName"] = contentAssetName
}
if let videoAnalytics = self.endpoints?.videoAnalytics {
info["c3.csid"] = videoAnalytics.getSessionId()
}
return info
}

private func calculatedAdTechnology(_ integrationKind: AdIntegrationKind) -> AdTechnology {
switch integrationKind {
case AdIntegrationKind.theoads:
Expand All @@ -31,10 +49,10 @@ class AdHandler {
return .SERVER_SIDE
}
}

private func AdTechnologyAsString(_ integration: AdIntegrationKind) -> String {
if integration == AdIntegrationKind.theoads {
return "Server Guided"
return Self.sgaiAdTechnology
}
let adTechnology = self.calculatedAdTechnology(integration)
switch adTechnology {
Expand Down Expand Up @@ -82,10 +100,12 @@ class AdHandler {

func adBreakBegin(event: AdBreakBeginEvent) {
log("handling adBreakBegin")
self.isAdBreakActive = true
guard let adBreak = event.ad else { return }
self.adBreakCounter += 1
let adBreakInfo = [
CIS_SSDK_AD_BREAK_POD_DURATION: Self.serialize(number: .init(value: adBreak.maxDuration)),
CIS_SSDK_AD_BREAK_POD_INDEX: Self.serialize(number: .init(value: adBreak.timeOffset)),
CIS_SSDK_AD_BREAK_POD_INDEX: Self.serialize(number: .init(value: self.adBreakCounter)),
CIS_SSDK_AD_BREAK_POD_POSITION: adBreak.calculateCurrentAdBreakPosition(),
"podTechnology": self.AdTechnologyAsString(adBreak.integration)
]
Expand All @@ -99,6 +119,7 @@ class AdHandler {

func adBreakEnd(event: AdBreakEndEvent) {
log("handling adBreakEnd")
self.isAdBreakActive = false
log("videoAnalytics.reportAdBreakEnded")
self.endpoints?.videoAnalytics.reportAdBreakEnded()
}
Expand All @@ -112,16 +133,10 @@ class AdHandler {
let adTechnology = self.AdTechnologyAsString(ad.integration)
// set Ad technology
info["c3.ad.technology"] = adTechnology

// set Ad contentAssetName
if let contentAssetName = self.storage?.metadataEntryForKey(CIS_SSDK_METADATA_ASSET_NAME) {
info["contentAssetName"] = contentAssetName
}
// set Ad session ID
if let videoAnalytics = self.endpoints?.videoAnalytics {
info["c3.csid"] = videoAnalytics.getSessionId()
}


// attach the ad session to the content session
self.collectBaseAdMetadata().forEach { info[$0.key] = $0.value }

// Temporary workaround for missing LinearAd in Native THEOplayerGoogleIMAIntegration. Can be removed after THEO-10161 is completed.
if !info.keys.contains(CIS_SSDK_METADATA_IS_LIVE), let duration = event.duration {
if duration.isInfinite {
Expand Down Expand Up @@ -174,6 +189,35 @@ class AdHandler {
}
}

/// Reports an ad break that failed before any ad became available, for example when the ad server
/// returns an empty VAST response for a server-guided (THEOads) ad break. No ad break or ad events
/// are dispatched in that case, so report it here to keep Conviva's ad attempt and fill rate metrics correct.
func reportFailedAdBreak(message: String, podDuration: Double?, podPosition: String) {
guard !self.isAdBreakActive else { return }
self.adBreakCounter += 1
let adBreakInfo: [String: Any] = [
CIS_SSDK_AD_BREAK_POD_DURATION: Self.serialize(number: .init(value: podDuration ?? 0)),
CIS_SSDK_AD_BREAK_POD_INDEX: Self.serialize(number: .init(value: self.adBreakCounter)),
CIS_SSDK_AD_BREAK_POD_POSITION: podPosition,
"podTechnology": Self.sgaiAdTechnology
]
log("videoAnalytics.reportAdBreakStarted: \(adBreakInfo)")
self.endpoints?.videoAnalytics.reportAdBreakStarted(
.ADPLAYER_CONTENT,
adType: .SERVER_SIDE,
adBreakInfo: adBreakInfo
)

var info = self.collectBaseAdMetadata()
info["c3.ad.technology"] = Self.sgaiAdTechnology
log("adAnalytics.setAdInfo: \(info)")
self.endpoints?.adAnalytics.setAdInfo(info)
log("adAnalytics.reportAdFailed: \(message)")
self.endpoints?.adAnalytics.reportAdFailed(message, adInfo: info)
log("videoAnalytics.reportAdBreakEnded")
self.endpoints?.videoAnalytics.reportAdBreakEnded()
}

static func createSerializationFormatter() -> NumberFormatter {
let formatter = NumberFormatter()
formatter.usesGroupingSeparator = false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// AdHandler+THEOads.swift
//

import ConvivaSDK
import THEOplayerSDK

#if canImport(THEOplayerTHEOadsIntegration)
import THEOplayerTHEOadsIntegration

extension AdHandler {
/// A THEOads (SGAI) ad break can fail before any ad is available, for example when the ad server
/// returns an empty VAST response. No ad break or ad events are dispatched in that case, so report
/// the attempted ad break as a failed ad to keep Conviva's ad attempt and fill rate metrics correct.
func interstitialError(event: THEOplayerTHEOadsIntegration.InterstitialErrorEvent) {
let interstitial = event.interstitial
guard interstitial.type == .adbreak else { return }
self.reportFailedAdBreak(
message: event.message ?? "No ad available",
podDuration: interstitial.duration,
podPosition: Self.calculateInterstitialAdBreakPosition(startTime: interstitial.startTime)
)
}

/// The position of a THEOads interstitial based on its start time.
static func calculateInterstitialAdBreakPosition(startTime: Double) -> String {
if startTime == 0 {
return "Pre-roll"
} else if startTime < 0 || !startTime.isFinite {
return "Post-roll"
} else {
return "Mid-roll"
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// THEOadsEventForwarder.swift
//

import THEOplayerSDK
import THEOplayerConnectorUtilities
#if canImport(THEOplayerTHEOadsIntegration)
import THEOplayerTHEOadsIntegration

/// A handle that registers THEOads listeners on a theoplayer and removes them on deinit
class THEOadsEventForwarder {
private let theoadsObserver: DispatchObserver?

init(player: THEOplayer, handler: AdHandler) {
if let theoads = player.theoads {
self.theoadsObserver = .init(
dispatcher: theoads,
eventListeners: Self.forwardEvents(from: theoads, to: handler)
)
} else {
self.theoadsObserver = nil
}
}

static func forwardEvents<Dispatcher: EventDispatcherProtocol>(from theoads: Dispatcher, to handler: AdHandler) -> [RemovableEventListenerProtocol] {
[
theoads.addRemovableEventListener(type: THEOadsEventTypes.INTERSTITIAL_ERROR) { event in
handler.interstitialError(event: event)
}
]
}
}
#endif
7 changes: 7 additions & 0 deletions THEOplayer-Connector-Conviva.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ Pod::Spec.new do |s|
live.dependency 'THEOplayer-Integration-THEOlive', "~> 11"
end

# --- Subspec: THEOads (+THEOads Conviva) ---
s.subspec 'THEOads' do |theoads|
theoads.source_files = 'Code/Conviva/Source/THEOads/**/*'
theoads.dependency "#{s.name}/Base"
theoads.dependency 'THEOplayer-Integration-THEOads', "~> 11"
end

# Default
s.default_subspecs = ['Base', 'THEOlive']
end
Loading