Skip to content

Commit

Permalink
Merge pull request #86 from bitmovin/feature/add-late-player-attaching
Browse files Browse the repository at this point in the history
Support late player attaching
  • Loading branch information
stonko1994 authored Aug 26, 2024
2 parents d54be38 + cb62c15 commit 07be1f0
Show file tree
Hide file tree
Showing 12 changed files with 343 additions and 92 deletions.
132 changes: 100 additions & 32 deletions BitmovinConvivaAnalytics/Classes/ConvivaAnalytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ private let notAvailable = "NA"

public final class ConvivaAnalytics: NSObject {
// MARK: - Bitmovin Player attributes
private let player: Player
private var player: Player?

// MARK: - Conviva related attributes
private let customerKey: String
Expand All @@ -31,7 +31,7 @@ public final class ConvivaAnalytics: NSObject {

// MARK: - Helper
private let logger: Logger
private let playerHelper: BitmovinPlayerHelper
private var playerHelper: BitmovinPlayerHelper?
// Workaround for player issue when onPlay is sent while player is stalled
private var isStalled = false
private var playbackStarted = false
Expand Down Expand Up @@ -66,21 +66,48 @@ public final class ConvivaAnalytics: NSObject {
}

// MARK: - initializer
/**
Initialize a new Bitmovin Conviva Analytics object to track metrics from Bitmovin Player

- Parameters:
- player: Bitmovin Player instance to track
- customerKey: Conviva customerKey
- config: ConvivaConfiguration object (see ConvivaConfiguration for more information)
*/
public init?(
/// Initialize a new Bitmovin Conviva Analytics object to track metrics from Bitmovin Player
///
/// - Parameters:
/// - player: Bitmovin Player instance to track
/// - customerKey: Conviva customerKey
/// - config: ConvivaConfiguration object (see ConvivaConfiguration for more information)
public convenience init(
player: Player,
customerKey: String,
config: ConvivaConfiguration = ConvivaConfiguration()
) throws {
self.player = player
self.playerHelper = BitmovinPlayerHelper(player: player)
try self.init(
player,
customerKey: customerKey,
config: config
)
}

/// Initialize a new Bitmovin Conviva Analytics object to track metrics from Bitmovin Player.
///
/// Use this initializer if you plan to manually start VST tracking **before** a `Player` instance is created.
/// Once the `Player` instance is created, attach it using the `attach(player:)` method.
///
/// - Parameters:
/// - customerKey: Conviva customerKey
/// - config: ConvivaConfiguration object (see ConvivaConfiguration for more information)
public convenience init(
customerKey: String,
config: ConvivaConfiguration = ConvivaConfiguration()
) throws {
try self.init(
nil,
customerKey: customerKey,
config: config
)
}

private init(
_ player: Player?,
customerKey: String,
config: ConvivaConfiguration
) throws {
self.customerKey = customerKey
self.config = config

Expand All @@ -99,8 +126,10 @@ public final class ConvivaAnalytics: NSObject {
adAnalytics = analytics.createAdAnalytics(withVideoAnalytics: videoAnalytics)
super.init()

listener = BitmovinPlayerListener(player: player)
listener?.delegate = self
if let player {
attach(player: player)
}

adAnalytics.setUpdateHandler { [weak self] in
self?.handleAdUpdateRequest()
}
Expand Down Expand Up @@ -181,7 +210,7 @@ public final class ConvivaAnalytics: NSObject {
return
}

if player.source?.sourceConfig.title == nil, contentMetadataBuilder.assetName == nil {
if player?.source?.sourceConfig.title == nil, contentMetadataBuilder.assetName == nil {
throw ConvivaAnalyticsError(
"AssetName is missing. Load player source (with title) first or set assetName via updateContentMetadata"
)
Expand Down Expand Up @@ -261,14 +290,50 @@ public final class ConvivaAnalytics: NSObject {
self.isBumper = false
logger.debugLog(message: "Tracking resumed.")
}

/// Attaches a `Player` instance to the Conviva Analytics object.
/// This method should be called as soon as the `Player` instance is initialized to not miss any tracking.
///
/// Has no effect if there is already a `Player` instance set. Use the `ConvivaAnalytics.init` without `player`
/// if you plan to attach a `Player` instance later in the life-cycle.
public func attach(player: Player) {
if self.player != nil {
logger.debugLog(
message: "[ Warning ] There is already a Player instance attached! Ignoring new Player instance."
)
return
}

if player.source != nil {
logger.debugLog(
message: """
[ Warning ] There is already a Source loaded into the Player instance! \
This method should be called before loading a Source.
"""
)
}

self.player = player
updateSession()

playerHelper = BitmovinPlayerHelper(player: player)
listener = BitmovinPlayerListener(player: player)
listener?.delegate = self
}
}

private extension ConvivaAnalytics {
private var isAd: Bool {
player.isAd || isSsaiAdBreakActive
guard let player else { return false }

return player.isAd || isSsaiAdBreakActive
}

private var currentPlayerState: ConvivaSDK.PlayerState {
guard let player else {
return .CONVIVA_STOPPED
}

if player.isPaused {
return .CONVIVA_PAUSED
}
Expand All @@ -287,7 +352,7 @@ private extension ConvivaAnalytics {
)
var playerInfo = [String: Any]()
playerInfo[CIS_SSDK_PLAYER_FRAMEWORK_NAME] = "Bitmovin Player iOS"
playerInfo[CIS_SSDK_PLAYER_FRAMEWORK_VERSION] = playerHelper.version
playerInfo[CIS_SSDK_PLAYER_FRAMEWORK_VERSION] = BitmovinPlayerHelper.version
videoAnalytics.setPlayerInfo(playerInfo)
adAnalytics.setAdPlayerInfo(playerInfo)
}
Expand Down Expand Up @@ -317,7 +382,7 @@ private extension ConvivaAnalytics {
}
buildDynamicContentMetadata()

if let videoQuality = player.videoQuality {
if let player, let videoQuality = player.videoQuality {
let bitrate = Int(videoQuality.bitrate) / 1_000 // in kbps
let value = NSValue(
cgSize: CGSize(
Expand Down Expand Up @@ -377,14 +442,17 @@ private extension ConvivaAnalytics {

// MARK: - meta data handling
private func buildContentMetadata() {
let sourceConfig = player.source?.sourceConfig
let sourceConfig = player?.source?.sourceConfig
contentMetadataBuilder.assetName = sourceConfig?.title

let customInternTags: [String: Any] = [
"streamType": playerHelper.streamType,
var customInternTags: [String: Any] = [
"integrationVersion": version
]

if let playerHelper {
customInternTags["streamType"] = playerHelper.streamType
}

contentMetadataBuilder.custom = customInternTags
buildDynamicContentMetadata()
}
Expand All @@ -406,7 +474,7 @@ private extension ConvivaAnalytics {
}

private func buildDynamicContentMetadata() {
guard let source = player.source else { return }
guard let player, let source = player.source else { return }

contentMetadataBuilder.duration = player.isLive ? -1 : Int(source.duration)
contentMetadataBuilder.streamType = player.isLive ? .CONVIVA_STREAM_LIVE : .CONVIVA_STREAM_VOD
Expand Down Expand Up @@ -450,7 +518,7 @@ private extension ConvivaAnalytics {
}

private func handleAdUpdateRequest() {
guard isAd else { return }
guard let player, isAd else { return }

adAnalytics.reportPlaybackMetric(
CIS_SSDK_PLAYBACK_METRIC_PLAY_HEAD_TIME,
Expand All @@ -459,15 +527,15 @@ private extension ConvivaAnalytics {
}

private func reportPlayHeadTime() {
guard isSessionActive else { return }
guard let player, isSessionActive else { return }

videoAnalytics.reportPlaybackMetric(
CIS_SSDK_PLAYBACK_METRIC_PLAY_HEAD_TIME,
value: Int64(player.currentTime(.relativeTime) * 1_000)
)
}

private func buildAdInfo(adStartedEvent: AdStartedEvent) -> [String: Any] {
private func buildAdInfo(adStartedEvent: AdStartedEvent, player: Player) -> [String: Any] {
var adInfo = [String: Any]()

adInfo["c3.ad.id"] = notAvailable
Expand All @@ -485,7 +553,7 @@ private extension ConvivaAnalytics {
.imaSdkVersion ?? notAvailable
} else {
adInfo[CIS_SSDK_PLAYER_FRAMEWORK_NAME] = "Bitmovin"
adInfo[CIS_SSDK_PLAYER_FRAMEWORK_VERSION] = playerHelper.version
adInfo[CIS_SSDK_PLAYER_FRAMEWORK_VERSION] = BitmovinPlayerHelper.version
}

adInfo["c3.ad.position"] = AdEventUtil.parseAdPosition(
Expand Down Expand Up @@ -556,7 +624,7 @@ extension ConvivaAnalytics: BitmovinPlayerListenerDelegate {
updateSession()
}

func onTimeChanged() {
func onTimeChanged(player: Player) {
guard !player.isAd else {
return
}
Expand Down Expand Up @@ -631,7 +699,7 @@ extension ConvivaAnalytics: BitmovinPlayerListenerDelegate {
}
}

func onStallEnded() {
func onStallEnded(player: Player) {
isStalled = false

guard playbackStarted else { return }
Expand All @@ -653,7 +721,7 @@ extension ConvivaAnalytics: BitmovinPlayerListenerDelegate {
videoAnalytics.reportPlaybackMetric(CIS_SSDK_PLAYBACK_METRIC_SEEK_STARTED, value: Int64(event.to.time * 1_000))
}

func onSeeked() {
func onSeeked(player: Player) {
if !isSessionActive {
// See comment in onSeek
return
Expand Down Expand Up @@ -684,8 +752,8 @@ extension ConvivaAnalytics: BitmovinPlayerListenerDelegate {
}

// MARK: - Ad events
func onAdStarted(_ event: AdStartedEvent) {
let adInfo = buildAdInfo(adStartedEvent: event)
func onAdStarted(_ event: AdStartedEvent, player: Player) {
let adInfo = buildAdInfo(adStartedEvent: event, player: player)
adAnalytics.reportAdLoaded(adInfo)
adAnalytics.reportAdStarted(adInfo)
adAnalytics.reportAdMetric(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ final class BitmovinPlayerHelper: NSObject {
}
}

var version: String {
static var version: String {
PlayerFactory.sdkVersion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ extension BitmovinPlayerListener: PlayerListener {
}

func onTimeChanged(_ event: TimeChangedEvent, player: Player) {
delegate?.onTimeChanged()
delegate?.onTimeChanged(player: player)
}

func onPlayerError(_ event: PlayerErrorEvent, player: Player) {
Expand Down Expand Up @@ -89,7 +89,7 @@ extension BitmovinPlayerListener: PlayerListener {
}

func onStallEnded(_ event: StallEndedEvent, player: Player) {
delegate?.onStallEnded()
delegate?.onStallEnded(player: player)
}

// MARK: - Seek / Timeshift events
Expand All @@ -98,7 +98,7 @@ extension BitmovinPlayerListener: PlayerListener {
}

func onSeeked(_ event: SeekedEvent, player: Player) {
delegate?.onSeeked()
delegate?.onSeeked(player: player)
}

func onTimeShift(_ event: TimeShiftEvent, player: Player) {
Expand All @@ -111,7 +111,7 @@ extension BitmovinPlayerListener: PlayerListener {

// MARK: - Ad events
func onAdStarted(_ event: AdStartedEvent, player: Player) {
delegate?.onAdStarted(event)
delegate?.onAdStarted(event, player: player)
}

func onAdFinished(_ event: AdFinishedEvent, player: Player) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ protocol BitmovinPlayerListenerDelegate: AnyObject {
func onEvent(_ event: Event)
func onSourceUnloaded()
func onSourceLoaded()
func onTimeChanged()
func onTimeChanged(player: Player)
func onPlayerError(_ event: PlayerErrorEvent)
func onSourceError(_ event: SourceErrorEvent)

Expand All @@ -26,16 +26,16 @@ protocol BitmovinPlayerListenerDelegate: AnyObject {
func onPaused()
func onPlaybackFinished()
func onStallStarted()
func onStallEnded()
func onStallEnded(player: Player)

// MARK: - Seek / Timeshift events
func onSeek(_ event: SeekEvent)
func onSeeked()
func onSeeked(player: Player)
func onTimeShift(_ event: TimeShiftEvent)
func onTimeShifted()

// MARK: - Ad events
func onAdStarted(_ event: AdStartedEvent)
func onAdStarted(_ event: AdStartedEvent, player: Player)
func onAdFinished()
func onAdSkipped(_ event: AdSkippedEvent)
func onAdError(_ event: AdErrorEvent)
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Added
- Warning logs in case the `viewerId` or `applicationName` is missing
- Possibility to start session tracking without a `Player` instance
- `ConvivaAnalytics.init(customerKey:config:)` initializer without a `Player`
- `ConvivaAnalytics.attach(player:)` to attach the `Player` at a later point in the session life-cycle

### Fixed
- Wrong initial Content Length reported when pre-roll ads are present
Expand Down
4 changes: 4 additions & 0 deletions Example/BitmovinConvivaAnalytics.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
37756EC2216F9AA50041806D /* CustomEventsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37756EC1216F9AA50041806D /* CustomEventsTest.swift */; };
37756EC42170764E0041806D /* SpecHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37756EC32170764E0041806D /* SpecHelper.swift */; };
37B615A02C775567009511D9 /* SourceTestDouble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B6159F2C77555E009511D9 /* SourceTestDouble.swift */; };
37C28E5B2C7885E800737FBC /* LatePlayerAttachingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C28E5A2C7885E800737FBC /* LatePlayerAttachingTest.swift */; };
37EB3CD3216B70D70093F085 /* TestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EB3CD1216B70650093F085 /* TestHelper.swift */; };
37EB3CD4216B70DA0093F085 /* BitmovinPlayerTestDouble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EB3CC7216B6E3F0093F085 /* BitmovinPlayerTestDouble.swift */; };
44D4DC502841032B00C0BE9D /* CISAnalyticsCreatorTestDouble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D4DC4F2841032B00C0BE9D /* CISAnalyticsCreatorTestDouble.swift */; };
Expand Down Expand Up @@ -84,6 +85,7 @@
37756EC32170764E0041806D /* SpecHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecHelper.swift; sourceTree = "<group>"; };
37B6159E2C774236009511D9 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = SOURCE_ROOT; };
37B6159F2C77555E009511D9 /* SourceTestDouble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceTestDouble.swift; sourceTree = "<group>"; };
37C28E5A2C7885E800737FBC /* LatePlayerAttachingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatePlayerAttachingTest.swift; sourceTree = "<group>"; };
37EB3CC7216B6E3F0093F085 /* BitmovinPlayerTestDouble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BitmovinPlayerTestDouble.swift; path = Doubles/BitmovinPlayerTestDouble.swift; sourceTree = "<group>"; };
37EB3CD1216B70650093F085 /* TestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelper.swift; sourceTree = "<group>"; };
3C5797E6D291A3F208E7499E /* Pods_BitmovinConvivaAnalytics_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BitmovinConvivaAnalytics_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -290,6 +292,7 @@
37756EAE216F8DF00041806D /* Helpers */,
37EB3CC6216B6D2B0093F085 /* Doubles */,
607FACE91AFB9204008FA782 /* Supporting Files */,
37C28E5A2C7885E800737FBC /* LatePlayerAttachingTest.swift */,
);
path = Tests;
sourceTree = "<group>";
Expand Down Expand Up @@ -749,6 +752,7 @@
37756EC2216F9AA50041806D /* CustomEventsTest.swift in Sources */,
44D4DC52284119E700C0BE9D /* CISAnalyticsTestDouble.swift in Sources */,
37EB3CD4216B70DA0093F085 /* BitmovinPlayerTestDouble.swift in Sources */,
37C28E5B2C7885E800737FBC /* LatePlayerAttachingTest.swift in Sources */,
44D4DC5428411DE400C0BE9D /* CISVideoAnalyticsTestDouble.swift in Sources */,
37B615A02C775567009511D9 /* SourceTestDouble.swift in Sources */,
37EB3CD3216B70D70093F085 /* TestHelper.swift in Sources */,
Expand Down
Loading

0 comments on commit 07be1f0

Please sign in to comment.