diff --git a/Classes/Events/PlayerEvent.swift b/Classes/Events/PlayerEvent.swift index a4391636..94ab8976 100644 --- a/Classes/Events/PlayerEvent.swift +++ b/Classes/Events/PlayerEvent.swift @@ -51,11 +51,11 @@ import AVFoundation /// Sent when source was selected. @objc public static let sourceSelected: PlayerEvent.Type = SourceSelected.self - /// Sent when an error occurs. + /// Sent when an error occurs in the player that the playback can recover from. @objc public static let error: PlayerEvent.Type = Error.self - /// Sent when an plugin error occurs. + /// Sent when a plugin error occurs. @objc public static let pluginError: PlayerEvent.Type = PluginError.self - /// Sent when an error log event received from player. + /// Sent when an error log event received from player (non fatal errors). @objc public static let errorLog: PlayerEvent.Type = ErrorLog.self // MARK: - Player Basic Events @@ -77,12 +77,8 @@ import AVFoundation class Seeked: PlayerEvent {} class SourceSelected: PlayerEvent { - convenience init(contentURL: URL?) { - guard let url = contentURL else { - self.init() - return - } - self.init([EventDataKeys.contentURL: url]) + convenience init(mediaSource: MediaSource) { + self.init([EventDataKeys.mediaSource: mediaSource]) } } diff --git a/Classes/PKError.swift b/Classes/PKError.swift index 25dfca54..8f0fe4ca 100644 --- a/Classes/PKError.swift +++ b/Classes/PKError.swift @@ -100,7 +100,7 @@ public enum PKPluginError: PKError { public var errorDescription: String { switch self { case .failedToCreatePlugin(let pluginName): return "failed to create plugin (\(pluginName)), doesn't exist in registry" - case .missingPluginConfig(let pluginName): return "Missing plugin config for plugin: \(pluginName)" + case .missingPluginConfig(let pluginName): return "Missing plugin config for plugin: \(pluginName) (wrong type or doesn't exist)" } } @@ -197,21 +197,6 @@ public extension PKError where Self: RawRepresentable, Self.RawValue == String { } } -/************************************************************/ -// MARK: - Error -/************************************************************/ -// extension for easier access to domain and code properties. -extension Error { - - public var domain: String { - return self._domain - } - - public var code: Int { - return self._code - } -} - /************************************************************/ // MARK: - PKError UserInfo Keys /************************************************************/ diff --git a/Classes/Player/PKEvent.swift b/Classes/Player/PKEvent.swift index 7a9718ea..79c89b7c 100644 --- a/Classes/Player/PKEvent.swift +++ b/Classes/Player/PKEvent.swift @@ -44,7 +44,7 @@ public extension PKEvent { static let newState = "newState" static let error = "error" static let metadata = "metadata" - static let contentURL = "contentURL" + static let mediaSource = "mediaSource" } // MARK: Player Data Accessors @@ -93,7 +93,7 @@ public extension PKEvent { } /// Content url, PKEvent Data Accessor - @objc public var contentURL: URL? { - return self.data?[EventDataKeys.contentURL] as? URL + @objc public var mediaSource: MediaSource? { + return self.data?[EventDataKeys.mediaSource] as? MediaSource } } diff --git a/Classes/Player/PlayerController.swift b/Classes/Player/PlayerController.swift index efe4aa79..eb711b0e 100644 --- a/Classes/Player/PlayerController.swift +++ b/Classes/Player/PlayerController.swift @@ -95,7 +95,7 @@ class PlayerController: NSObject, Player, PlayerSettings { // get the preferred media source and post source selected event guard let (preferredMediaSource, handlerType) = AssetBuilder.getPreferredMediaSource(from: mediaConfig.mediaEntry) else { return } - self.onEventBlock?(PlayerEvent.SourceSelected(contentURL: preferredMediaSource.playbackUrl)) + self.onEventBlock?(PlayerEvent.SourceSelected(mediaSource: preferredMediaSource)) self.preferredMediaSource = preferredMediaSource // update the media source request adapter with new media uuid if using kaltura request adapter diff --git a/Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift b/Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift index abb1e53b..088e4c08 100644 --- a/Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift +++ b/Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift @@ -8,46 +8,6 @@ import Foundation -/************************************************************/ -// MARK: - AnalyticsPluginError -/************************************************************/ - -/// `AnalyticsError` represents analytics plugins (kaltura stats, kaltura live stats, phoenix and tvpapi) common errors. -enum AnalyticsPluginError: PKError { - - case missingMediaEntry - case missingInitObject - - static let domain = "com.kaltura.playkit.error.analyticsPlugin" - - var code: Int { - switch self { - case .missingMediaEntry: return PKErrorCode.missingMediaEntry - case .missingInitObject: return PKErrorCode.missingInitObject - } - } - - var errorDescription: String { - switch self { - case .missingMediaEntry: return "failed to send analytics event, mediaEntry is nil" - case .missingInitObject: return "failed to send analytics event, missing initObj" - } - } - - var userInfo: [String: Any] { - return [:] - } -} - -extension PKErrorDomain { - @objc(AnalyticsPlugin) public static let analyticsPlugin = AnalyticsPluginError.domain -} - -extension PKErrorCode { - @objc(MissingMediaEntry) public static let missingMediaEntry = 2100 - @objc(MissingInitObject) public static let missingInitObject = 2101 -} - /************************************************************/ // MARK: - BaseAnalyticsPlugin /************************************************************/ diff --git a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift index ed69b48b..8e6cbf40 100644 --- a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift +++ b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift @@ -10,12 +10,15 @@ import Foundation import KalturaNetKit /// class `BaseOTTAnalyticsPlugin` is a base plugin object used for OTT analytics plugin subclasses -public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProtocol, AppStateObservable { +public class BaseOTTAnalyticsPlugin: BasePlugin, OTTAnalyticsPluginProtocol, AppStateObservable { + /// indicates whether we played for the first time or not. + var isFirstPlay: Bool = true var intervalOn: Bool = false var timer: Timer? var interval: TimeInterval = 30 var isContentEnded: Bool = false + var fileId: String? /************************************************************/ // MARK: - PKPlugin @@ -24,23 +27,26 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) throws { try super.init(player: player, pluginConfig: pluginConfig, messageBus: messageBus) AppStateSubject.shared.add(observer: self) + self.registerEvents() } public override func onUpdateMedia(mediaConfig: MediaConfig) { super.onUpdateMedia(mediaConfig: mediaConfig) self.intervalOn = false self.isContentEnded = false + self.isFirstPlay = true self.timer?.invalidate() } public override func destroy() { - super.destroy() + self.messageBus?.removeObserver(self, events: playerEventsToRegister) // only send stop event if content started playing already & content is not ended if !self.isFirstPlay && !self.isContentEnded { self.sendAnalyticsEvent(ofType: .stop) } self.timer?.invalidate() AppStateSubject.shared.remove(observer: self) + super.destroy() } /************************************************************/ @@ -61,18 +67,20 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt /************************************************************/ /// default events to register - override var playerEventsToRegister: [PlayerEvent.Type] { + var playerEventsToRegister: [PlayerEvent.Type] { return [ PlayerEvent.ended, PlayerEvent.error, PlayerEvent.pause, + PlayerEvent.stopped, PlayerEvent.loadedMetadata, PlayerEvent.playing, - PlayerEvent.seeked + PlayerEvent.seeked, + PlayerEvent.sourceSelected ] } - override func registerEvents() { + func registerEvents() { PKLog.debug("plugin \(type(of:self)) register to all player events") self.playerEventsToRegister.forEach { event in @@ -83,7 +91,6 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt case let e where e.self == PlayerEvent.ended: self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in guard let strongSelf = self else { return } - PKLog.debug("ended event: \(event)") strongSelf.timer?.invalidate() strongSelf.sendAnalyticsEvent(ofType: .finish) strongSelf.isContentEnded = true @@ -91,13 +98,11 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt case let e where e.self == PlayerEvent.error: self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in guard let strongSelf = self else { return } - PKLog.debug("error event: \(event)") strongSelf.sendAnalyticsEvent(ofType: .error) } case let e where e.self == PlayerEvent.pause: self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in guard let strongSelf = self else { return } - PKLog.debug("pause event: \(event)") // invalidate timer when receiving pause event only after first play // and set intervalOn to false in order to start timer again on play event. if !strongSelf.isFirstPlay { @@ -106,16 +111,19 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt } strongSelf.sendAnalyticsEvent(ofType: .pause) } + case let e where e.self == PlayerEvent.stopped: + self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in + guard let strongSelf = self else { return } + strongSelf.cancelTimer() + } case let e where e.self == PlayerEvent.loadedMetadata: self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in guard let strongSelf = self else { return } - PKLog.debug("loadedMetadata event: \(event)") strongSelf.sendAnalyticsEvent(ofType: .load) } case let e where e.self == PlayerEvent.playing: self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in guard let strongSelf = self else { return } - PKLog.debug("play event: \(event)") if !strongSelf.intervalOn { strongSelf.createTimer() @@ -129,6 +137,12 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt strongSelf.sendAnalyticsEvent(ofType: .play); } } + case let e where e.self == PlayerEvent.sourceSelected: + self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in + guard let strongSelf = self else { return } + guard let mediaSource = event.mediaSource else { return } + strongSelf.fileId = mediaSource.id + } default: assertionFailure("plugin \(type(of:self)) all events must be handled") } } @@ -173,10 +187,6 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt extension BaseOTTAnalyticsPlugin { fileprivate func createTimer() { - if let conf = self.config, let intr = conf.params["timerInterval"] as? TimeInterval { - self.interval = intr - } - if let t = self.timer { t.invalidate() } @@ -184,9 +194,17 @@ extension BaseOTTAnalyticsPlugin { // media hit should fire on every time we start the timer. self.sendProgressEvent() - self.timer = Timer.every(self.interval) { [unowned self] in + self.timer = Timer.every(self.interval) { [weak self] in PKLog.debug("timerHit") - self.sendProgressEvent() + self?.sendProgressEvent() + } + } + + fileprivate func cancelTimer() { + if let t = self.timer { + t.invalidate() + self.timer = nil + self.intervalOn = false } } diff --git a/Plugins/Phoenix/MediaMarkService.swift b/Plugins/Phoenix/MediaMarkService.swift index 06f3073b..dbd4fd36 100644 --- a/Plugins/Phoenix/MediaMarkService.swift +++ b/Plugins/Phoenix/MediaMarkService.swift @@ -10,9 +10,9 @@ import UIKit import SwiftyJSON import KalturaNetKit -internal class MediaMarkService { +class MediaMarkService { - internal static func sendTVPAPIEVent(baseURL: String, + static func sendTVPAPIEVent(baseURL: String, initObj: [String: Any], eventType: String, currentTime: Int32, @@ -31,20 +31,8 @@ internal class MediaMarkService { request.setBody(key: "Action", value: JSON(eventType)) } return request - }else{ + } else { return nil } - - } - - private static func createBookmark(eventType: String, position: Int32, assetId: String, fileId: String) -> JSON { - var json: JSON = JSON.init(["objectType": "KalturaBookmark"]) - json["type"] = JSON("media") - json["id"] = JSON(assetId) - json["position"] = JSON(position) - json["playerData"] = JSON.init(["action": JSON(eventType), "objectType": JSON("KalturaBookmarkPlayerData"), "fileId": JSON(fileId)]) - - - return json } } diff --git a/Plugins/Phoenix/OTTAnalyticsPluginConfig.swift b/Plugins/Phoenix/OTTAnalyticsPluginConfig.swift new file mode 100644 index 00000000..3251ac45 --- /dev/null +++ b/Plugins/Phoenix/OTTAnalyticsPluginConfig.swift @@ -0,0 +1,42 @@ +// +// OTTAnalyticsConfig.swift +// Pods +// +// Created by Gal Orlanczyk on 25/06/2017. +// +// + +import Foundation + +@objc public class OTTAnalyticsPluginConfig: NSObject { + + let baseUrl: String + let timerInterval: TimeInterval + + init(baseUrl: String, timerInterval: TimeInterval) { + self.baseUrl = baseUrl + self.timerInterval = timerInterval + } +} + +@objc public class PhoenixAnalyticsPluginConfig: OTTAnalyticsPluginConfig { + + let ks: String + let partnerId: Int + + @objc public init(baseUrl: String, timerInterval: TimeInterval, ks: String, partnerId: Int) { + self.ks = ks + self.partnerId = partnerId + super.init(baseUrl: baseUrl, timerInterval: timerInterval) + } +} + +@objc public class TVPAPIAnalyticsPluginConfig: OTTAnalyticsPluginConfig { + + let initObject: [String: Any] + + @objc public init(baseUrl: String, timerInterval: TimeInterval, initObject: [String: Any]) { + self.initObject = initObject + super.init(baseUrl: baseUrl, timerInterval: timerInterval) + } +} diff --git a/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift b/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift index f80e6480..8604a5c0 100644 --- a/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift +++ b/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift @@ -13,32 +13,41 @@ public class PhoenixAnalyticsPlugin: BaseOTTAnalyticsPlugin { public override class var pluginName: String { return "PhoenixAnalytics" } - /************************************************************/ - // MARK: - KalturaOTTAnalyticsPluginProtocol - /************************************************************/ - - override func buildRequest(ofType type: OTTAnalyticsEventType) -> Request? { - var fileId = "" - var baseUrl = "" - var ks = "" - var parterId = 0 - - if let url = self.config?.params["baseUrl"] as? String { - baseUrl = url + var config: PhoenixAnalyticsPluginConfig! { + didSet { + self.interval = config.timerInterval } - - if let fId = self.config?.params["fileId"] as? String { - fileId = fId - } - - if let theKs = self.config?.params["ks"] as? String { - ks = theKs + } + + public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) throws { + try super.init(player: player, pluginConfig: pluginConfig, messageBus: messageBus) + guard let config = pluginConfig as? PhoenixAnalyticsPluginConfig else { + PKLog.error("missing/wrong plugin config") + throw PKPluginError.missingPluginConfig(pluginName: PhoenixAnalyticsPlugin.pluginName) } + self.config = config + self.interval = config.timerInterval + self.registerEvents() + } + + public override func onUpdateConfig(pluginConfig: Any) { + super.onUpdateConfig(pluginConfig: pluginConfig) - if let pId = self.config?.params["partnerId"] as? Int { - parterId = pId + guard let config = pluginConfig as? PhoenixAnalyticsPluginConfig else { + PKLog.error("plugin config is wrong") + return } + PKLog.debug("new config::\(String(describing: config))") + self.config = config + } + + /************************************************************/ + // MARK: - KalturaOTTAnalyticsPluginProtocol + /************************************************************/ + + override func buildRequest(ofType type: OTTAnalyticsEventType) -> Request? { + guard let player = self.player else { PKLog.error("send analytics failed due to nil associated player") return nil @@ -46,18 +55,17 @@ public class PhoenixAnalyticsPlugin: BaseOTTAnalyticsPlugin { guard let mediaEntry = player.mediaEntry else { PKLog.error("send analytics failed due to nil mediaEntry") - self.messageBus?.post(PlayerEvent.PluginError(error: AnalyticsPluginError.missingMediaEntry)) return nil } - guard let requestBuilder: KalturaRequestBuilder = BookmarkService.actionAdd(baseURL: baseUrl, - partnerId: parterId, - ks: ks, - eventType: type.rawValue.uppercased(), - currentTime: player.currentTime.toInt32(), - assetId: mediaEntry.id, - fileId: fileId) else { - return nil + guard let requestBuilder: KalturaRequestBuilder = BookmarkService.actionAdd(baseURL: config.baseUrl, + partnerId: config.partnerId, + ks: config.ks, + eventType: type.rawValue.uppercased(), + currentTime: player.currentTime.toInt32(), + assetId: mediaEntry.id, + fileId: fileId ?? "") else { + return nil } requestBuilder.set { (response: Response) in diff --git a/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift b/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift index 629a6baf..6f207499 100644 --- a/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift +++ b/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift @@ -14,6 +14,35 @@ public class TVPAPIAnalyticsPlugin: BaseOTTAnalyticsPlugin { public override class var pluginName: String { return "TVPAPIAnalytics" } + var config: TVPAPIAnalyticsPluginConfig! { + didSet { + self.interval = config.timerInterval + } + } + + public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) throws { + try super.init(player: player, pluginConfig: pluginConfig, messageBus: messageBus) + guard let config = pluginConfig as? TVPAPIAnalyticsPluginConfig else { + PKLog.error("missing/wrong plugin config") + throw PKPluginError.missingPluginConfig(pluginName: TVPAPIAnalyticsPlugin.pluginName) + } + self.config = config + self.interval = config.timerInterval + self.registerEvents() + } + + public override func onUpdateConfig(pluginConfig: Any) { + super.onUpdateConfig(pluginConfig: pluginConfig) + + guard let config = pluginConfig as? TVPAPIAnalyticsPluginConfig else { + PKLog.error("plugin config is wrong") + return + } + + PKLog.debug("new config::\(String(describing: config))") + self.config = config + } + /************************************************************/ // MARK: - KalturaOTTAnalyticsPluginProtocol /************************************************************/ @@ -21,38 +50,21 @@ public class TVPAPIAnalyticsPlugin: BaseOTTAnalyticsPlugin { override func buildRequest(ofType type: OTTAnalyticsEventType) -> Request? { guard let player = self.player else { return nil } - var fileId = "" - var baseUrl = "" - - guard let initObj = self.config?.params["initObj"] as? [String: Any] else { - PKLog.error("send analytics failed due to no initObj data") - self.messageBus?.post(PlayerEvent.PluginError(error: AnalyticsPluginError.missingInitObject)) - return nil - } - guard let mediaEntry = player.mediaEntry else { PKLog.error("send analytics failed due to nil mediaEntry") - self.messageBus?.post(PlayerEvent.PluginError(error: AnalyticsPluginError.missingMediaEntry)) return nil } let method = type == .hit ? "MediaHit" : "MediaMark" - - if let url = self.config?.params["baseUrl"] as? String { - baseUrl = url - } - if let fId = self.config?.params["fileId"] as? String { - fileId = fId - } - baseUrl = "\(baseUrl)m=\(method)" + let baseUrl = "\(self.config.baseUrl)m=\(method)" guard let requestBuilder: RequestBuilder = MediaMarkService.sendTVPAPIEVent(baseURL: baseUrl, - initObj: initObj, + initObj: self.config.initObject, eventType: type.rawValue, currentTime: player.currentTime.toInt32(), assetId: mediaEntry.id, - fileId: fileId) else { + fileId: self.fileId ?? "") else { return nil } requestBuilder.set(responseSerializer: StringSerializer()) diff --git a/Plugins/Youbora/YouboraManager.swift b/Plugins/Youbora/YouboraManager.swift index e7f9fc52..f0cbd1d5 100644 --- a/Plugins/Youbora/YouboraManager.swift +++ b/Plugins/Youbora/YouboraManager.swift @@ -227,7 +227,7 @@ extension YouboraManager { case let e where e.self == PlayerEvent.sourceSelected: messageBus.addObserver(self, events: [e.self]) { [weak self] event in guard let strongSelf = self else { return } - self?.lastReportedResource = event.contentURL?.absoluteString + self?.lastReportedResource = event.mediaSource?.playbackUrl?.absoluteString strongSelf.postEventLog(withMessage: "\(event.namespace))") } case let e where e.self == PlayerEvent.error: