From 6cf6637ae15b3a33a495280f1c89519a02030367 Mon Sep 17 00:00:00 2001 From: ElizaSapir Date: Tue, 25 Apr 2017 16:39:53 +0300 Subject: [PATCH 01/13] Add supported version to Podspec dependencies (#147) * Add supported version to Podspec dependencies * FEM-1358 #comment update version --- PlayKit.podspec | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PlayKit.podspec b/PlayKit.podspec index 288659d5..8dbbd343 100644 --- a/PlayKit.podspec +++ b/PlayKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'PlayKit' -s.version = '0.1.x-dev' +s.version = '0.2.x-dev' s.summary = 'PlayKit: Kaltura Mobile Player SDK - iOS' @@ -13,10 +13,10 @@ s.ios.deployment_target = '8.0' s.subspec 'Core' do |sp| sp.source_files = 'Classes/**/*' - sp.dependency 'SwiftyJSON' - sp.dependency 'Log' - sp.dependency 'SwiftyXMLParser' - sp.dependency 'KalturaNetKit' + sp.dependency 'SwiftyJSON', '3.1.4' + sp.dependency 'Log', '1.0' + sp.dependency 'SwiftyXMLParser', '3.0.0' + sp.dependency 'KalturaNetKit', '0.0.12' end s.subspec 'IMAPlugin' do |ssp| @@ -51,7 +51,7 @@ s.subspec 'YouboraPlugin' do |ssp| 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**', 'LIBRARY_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**' } - ssp.dependency 'Youbora-AVPlayer/dynamic' + ssp.dependency 'Youbora-AVPlayer/dynamic', '5.3.5' ssp.dependency 'PlayKit/Core' ssp.dependency 'PlayKit/AnalyticsCommon' end From cab27cba864628fc7e9bd8201b3654f8c9872d3d Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Tue, 25 Apr 2017 17:13:39 +0300 Subject: [PATCH 02/13] #FEM-1321 (#149) * Fixed issue with duration event firing more than once and having wrong values. --- .../AVPlayerEngine/AVPlayerEngine+AssetLoading.swift | 9 ++++++--- .../AVPlayerEngine/AVPlayerEngine+Observation.swift | 10 +++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift index 296c1c25..ccee3b86 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift @@ -11,6 +11,12 @@ import AVFoundation extension AVPlayerEngine { + override func replaceCurrentItem(with item: AVPlayerItem?) { + // When changing media (loading new asset) we want to reset isFirstReady in order to receive `CanPlay` & `LoadedMetadata` accuratly. + self.isFirstReady = true + super.replaceCurrentItem(with: item) + } + func asynchronouslyLoadURLAsset(_ newAsset: AVAsset) { /* Using AVAsset now runs the risk of blocking the current thread (the @@ -59,9 +65,6 @@ extension AVPlayerEngine { return } - - // When changing media (loading new asset) we want to reset isFirstReady in order to receive `CanPlay` & `LoadedMetadata` accuratly. - self.isFirstReady = true /* We can play this asset. Create a new `AVPlayerItem` and make diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift index a050e552..58076641 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift @@ -20,7 +20,6 @@ extension AVPlayerEngine { #keyPath(currentItem), #keyPath(currentItem.playbackLikelyToKeepUp), #keyPath(currentItem.playbackBufferEmpty), - #keyPath(currentItem.duration), #keyPath(currentItem.timedMetadata) ] } @@ -124,10 +123,6 @@ extension AVPlayerEngine { self.handleLikelyToKeepUp() case #keyPath(currentItem.playbackBufferEmpty): self.handleBufferEmptyChange() - case #keyPath(currentItem.duration): - if let currentItem = self.currentItem { - self.post(event: PlayerEvent.DurationChanged(duration: CMTimeGetSeconds(currentItem.duration))) - } case #keyPath(rate): self.handleRate() case #keyPath(currentItem.status): @@ -198,6 +193,11 @@ extension AVPlayerEngine { if self.isFirstReady { self.isFirstReady = false + // when player item is readyToPlay for the first time it is safe to assume we have a valid duration. + if let duration = self.currentItem?.duration, duration != kCMTimeIndefinite { + PKLog.debug("duration in seconds: \(CMTimeGetSeconds(duration))") + self.post(event: PlayerEvent.DurationChanged(duration: CMTimeGetSeconds(duration))) + } self.post(event: PlayerEvent.LoadedMetadata()) self.post(event: PlayerEvent.CanPlay()) } From 3510dc1cbd070705540c7390445601bd141c8184 Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Thu, 27 Apr 2017 17:22:22 +0300 Subject: [PATCH 03/13] #FEM-1361 (#150) * Fixed issue where `stop` event in OTT analytics was sent before content even played. --- Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift | 1 + Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift b/Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift index 26a3e805..abb1e53b 100644 --- a/Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift +++ b/Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift @@ -56,6 +56,7 @@ extension PKErrorCode { @objc public class BaseAnalyticsPlugin: BasePlugin, AnalyticsPluginProtocol { var config: AnalyticsConfig? + /// indicates whether we played for the first time or not. var isFirstPlay: Bool = true /************************************************************/ diff --git a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift index 4c1fa0a1..d152c637 100644 --- a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift +++ b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift @@ -33,7 +33,10 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt public override func destroy() { super.destroy() - self.sendAnalyticsEvent(ofType: .stop) + // only send stop event if content started playing already + if !self.isFirstPlay { + self.sendAnalyticsEvent(ofType: .stop) + } self.timer?.invalidate() AppStateSubject.shared.remove(observer: self) } From b5039e661fa8c719df1e18c7ec823cd1f64d127b Mon Sep 17 00:00:00 2001 From: ElizaSapir Date: Fri, 28 Apr 2017 12:41:55 +0300 Subject: [PATCH 04/13] update KalturaNetKit version (#151) --- PlayKit.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PlayKit.podspec b/PlayKit.podspec index 8dbbd343..095c366e 100644 --- a/PlayKit.podspec +++ b/PlayKit.podspec @@ -16,7 +16,7 @@ s.subspec 'Core' do |sp| sp.dependency 'SwiftyJSON', '3.1.4' sp.dependency 'Log', '1.0' sp.dependency 'SwiftyXMLParser', '3.0.0' - sp.dependency 'KalturaNetKit', '0.0.12' + sp.dependency 'KalturaNetKit', '~> 0.0' end s.subspec 'IMAPlugin' do |ssp| From 30e8f4d8ea6ce52452b7376c58aa3c862201bba8 Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Sun, 30 Apr 2017 12:22:17 +0300 Subject: [PATCH 05/13] #FEM 1366 (#152) * remove all references to play session id from any providers. --- Classes/Providers/OVP/OVPMediaProvider.swift | 1 - Classes/Providers/OVP/SourceBuilder.swift | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/Classes/Providers/OVP/OVPMediaProvider.swift b/Classes/Providers/OVP/OVPMediaProvider.swift index 77a14f44..3fd7d5f1 100644 --- a/Classes/Providers/OVP/OVPMediaProvider.swift +++ b/Classes/Providers/OVP/OVPMediaProvider.swift @@ -302,7 +302,6 @@ import KalturaNetKit .set(uiconfId: loadInfo.uiconfId?.int64Value) .set(flavors: source.flavors) .set(partnerId: loadInfo.sessionProvider.partnerId) - .set(playSessionId: UUID().uuidString) .set(sourceProtocol: source.protocols?.last) .set(fileExtension: formatType.fileExtension) .set(ks: ks) diff --git a/Classes/Providers/OVP/SourceBuilder.swift b/Classes/Providers/OVP/SourceBuilder.swift index ba360744..57a9f4e4 100644 --- a/Classes/Providers/OVP/SourceBuilder.swift +++ b/Classes/Providers/OVP/SourceBuilder.swift @@ -19,7 +19,6 @@ class SourceBuilder { var uiconfId:Int64? var format:String? = "url" var sourceProtocol:String? = "https" - var playSessionId:String? var drmSchemes:[String]? var fileExtension: String? @@ -72,13 +71,6 @@ class SourceBuilder { return self } - @discardableResult - func set(playSessionId:String?) -> SourceBuilder { - self.playSessionId = playSessionId - return self - } - - @discardableResult func set(drmSchemes:[String]?) -> SourceBuilder { self.drmSchemes = drmSchemes @@ -137,10 +129,6 @@ class SourceBuilder { var params: [String] = [String]() - if let playSessionId = self.playSessionId{ - params.append("playSessionId=" + playSessionId) - } - if flavorsExist == true , let uiconfId = self.uiconfId { params.append("/uiConfId/" + String(uiconfId)) } From 0fd423bf00b260b0d443825e5c1075a92c67917f Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Sun, 7 May 2017 17:40:42 +0300 Subject: [PATCH 06/13] Initial warning fix (#153) --- Addons/GoogleCast/BasicCastBuilder.swift | 31 ++++++++++--------- Addons/GoogleCast/CastAdInfoParser.swift | 30 ++++++++---------- Classes/Player/AssetLoaderDelegate.swift | 4 +-- Classes/Player/Player.swift | 2 +- Classes/Player/PlayerDecoratorBase.swift | 2 +- Classes/Player/Track.swift | 2 +- Classes/Player/TracksManager.swift | 2 +- .../Providers/OTT/PhoenixMediaProvider.swift | 29 ++++++++++++----- Plugins/Phoenix/PhoenixAnalyticsPlugin.swift | 2 +- Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift | 2 +- 10 files changed, 60 insertions(+), 46 deletions(-) diff --git a/Addons/GoogleCast/BasicCastBuilder.swift b/Addons/GoogleCast/BasicCastBuilder.swift index 1b76665b..2609a6c5 100644 --- a/Addons/GoogleCast/BasicCastBuilder.swift +++ b/Addons/GoogleCast/BasicCastBuilder.swift @@ -15,9 +15,11 @@ import GoogleCast */ @objc public class BasicCastBuilder: NSObject { + @objc public enum StreamType: Int { case live case vod + case unknown } enum BasicBuilderDataError: Error { @@ -33,23 +35,26 @@ import GoogleCast @objc public var partnerID: String? @objc public var uiconfID: String? @objc public var adTagURL: String? - @objc public private(set) var streamType = GCKMediaStreamType.none @objc public var metaData: GCKMediaMetadata? + @objc public var streamType = StreamType.unknown { + didSet { + switch streamType { + case .live: self.gckMediaStreamType = .live + case .vod: self.gckMediaStreamType = .buffered + case .unknown: self.gckMediaStreamType = .unknown + } + } + } + private var gckMediaStreamType = GCKMediaStreamType.unknown /** Set - stream type - Parameter contentId: receiver contentId to play ( Entry id, or Asset id ) */ @discardableResult - @objc public func set(streamType: StreamType) -> Self{ - - switch streamType { - case .live: - self.streamType = .live - case .vod: - self.streamType = .buffered - } + @nonobjc public func set(streamType: StreamType) -> Self { + self.streamType = streamType return self } @@ -58,7 +63,7 @@ import GoogleCast - Parameter contentId: receiver contentId to play ( Entry id, or Asset id ) */ @discardableResult - @nonobjc public func set(contentId: String?) -> Self{ + @nonobjc public func set(contentId: String?) -> Self { guard contentId != nil, contentId?.isEmpty == false @@ -169,11 +174,9 @@ import GoogleCast throw BasicCastBuilder.BasicBuilderDataError.missingContentId } - guard self.streamType != nil else { + guard self.streamType != .unknown else { throw BasicCastBuilder.BasicBuilderDataError.missingStreamType } - - } @@ -185,7 +188,7 @@ import GoogleCast try self.validate() let customData = self.customData() let mediaInfo: GCKMediaInformation = GCKMediaInformation(contentID:self.contentId, - streamType: self.streamType, + streamType: self.gckMediaStreamType, contentType: "", metadata: self.metaData, streamDuration: 0, diff --git a/Addons/GoogleCast/CastAdInfoParser.swift b/Addons/GoogleCast/CastAdInfoParser.swift index 82070bb6..00589230 100644 --- a/Addons/GoogleCast/CastAdInfoParser.swift +++ b/Addons/GoogleCast/CastAdInfoParser.swift @@ -27,15 +27,13 @@ public class CastAdInfoParser: NSObject, GCKRemoteMediaClientAdInfoParserDelegat */ public func remoteMediaClient(_ client: GCKRemoteMediaClient, shouldSetPlayingAdIn mediaStatus: GCKMediaStatus) -> Bool { - guard let customData = mediaStatus.customData as? [String:Any], - let adsInfo = customData["adsInfo"] as? [String:Any], - let metaData : AdsMetadata = AdsMetadata(dict: adsInfo) else { - PKLog.warning("No Ads info from receiver") - return false + guard let customData = mediaStatus.customData as? [String: Any], let adsInfo = customData["adsInfo"] as? [String: Any] else { + PKLog.warning("No Ads info from receiver") + return false } + let metadata = AdsMetadata(dict: adsInfo) - return metaData.isPlayingAd - + return metadata.isPlayingAd } /** @@ -43,15 +41,15 @@ public class CastAdInfoParser: NSObject, GCKRemoteMediaClientAdInfoParserDelegat */ public func remoteMediaClient(_ client: GCKRemoteMediaClient, shouldSetAdBreaksIn mediaStatus: GCKMediaStatus) -> [GCKAdBreakInfo]? { - guard let customData = mediaStatus.customData as? [String:Any], - let adsInfo = customData["adsInfo"] as? [String:Any], - let adsData : AdsMetadata = AdsMetadata(dict: adsInfo), - let adsBreakInfo = adsData.adsBreakInfo else { - PKLog.warning("No Ads info from receiver") - return nil + guard let customData = mediaStatus.customData as? [String: Any], let adsInfo = customData["adsInfo"] as? [String: Any] else { + PKLog.warning("No Ads info from receiver") + return nil } + let metadata = AdsMetadata(dict: adsInfo) + let adsBreakInfo = metadata.adsBreakInfo ?? [] let adsBreakInfoArray = adsBreakInfo.map({ GCKAdBreakInfo(playbackPosition: TimeInterval($0)) }) + return adsBreakInfoArray } } @@ -71,7 +69,7 @@ private class AdsMetadata: NSObject { if let isPlaying = dict["isPlayingAd"] as? Bool { self.isPlayingAd = isPlaying - }else{ + } else { self.isPlayingAd = false } @@ -79,10 +77,8 @@ private class AdsMetadata: NSObject { self.adsBreakInfo = adBreaksInfo.map({ (number:NSNumber) -> Int in return number.intValue }) - }else{ + } else { self.adsBreakInfo = nil } - - } } diff --git a/Classes/Player/AssetLoaderDelegate.swift b/Classes/Player/AssetLoaderDelegate.swift index 889c35ca..fab79081 100644 --- a/Classes/Player/AssetLoaderDelegate.swift +++ b/Classes/Player/AssetLoaderDelegate.swift @@ -135,7 +135,7 @@ class AssetLoaderDelegate: NSObject { do { let data = try self.storage?.load(key: persistentKeyName(assetId)) if data != nil { - PKLog.debug("Loaded PCKD with \(data?.count) bytes") + PKLog.debug("Loaded PCKD with \(String(describing: data?.count)) bytes") } else { PKLog.error("Load PCKD failed (1)") } @@ -230,7 +230,7 @@ class AssetLoaderDelegate: NSObject { spcData = try resourceLoadingRequest.streamingContentKeyRequestData(forApp: applicationCertificate, contentIdentifier: assetIDData, options: resourceLoadingRequestOptions) PKLog.debug("Got spcData with", spcData.count, "bytes") } catch let error as NSError { - PKLog.error("Error obtaining key request data: \(error.domain) reason: \(error.localizedFailureReason)") + PKLog.error("Error obtaining key request data: \(error.domain) reason: \(String(describing: error.localizedFailureReason))") resourceLoadingRequest.finishLoading(with: error) self.done?(error) return diff --git a/Classes/Player/Player.swift b/Classes/Player/Player.swift index e3b35e48..ff320161 100644 --- a/Classes/Player/Player.swift +++ b/Classes/Player/Player.swift @@ -16,7 +16,7 @@ import AVKit /// `PlayerSettings` used for optional `Player` settings. @objc public protocol PlayerSettings { - func set(contentRequestAdapter: PKRequestParamsAdapter) + func set(contentRequestAdapter: PKRequestParamsAdapter?) } @objc public protocol Player: PlayerSettings { diff --git a/Classes/Player/PlayerDecoratorBase.swift b/Classes/Player/PlayerDecoratorBase.swift index 227a2f96..6ed90242 100644 --- a/Classes/Player/PlayerDecoratorBase.swift +++ b/Classes/Player/PlayerDecoratorBase.swift @@ -128,7 +128,7 @@ import AVKit extension PlayerDecoratorBase: PlayerSettings { - public func set(contentRequestAdapter: PKRequestParamsAdapter) { + public func set(contentRequestAdapter: PKRequestParamsAdapter?) { self.player.set(contentRequestAdapter: contentRequestAdapter) } } diff --git a/Classes/Player/Track.swift b/Classes/Player/Track.swift index 83f29ba4..331b7207 100644 --- a/Classes/Player/Track.swift +++ b/Classes/Player/Track.swift @@ -14,7 +14,7 @@ import Foundation @objc public var language: String? init(id: String?, title: String?, language: String?) { - PKLog.debug("init:: id:\(id) title:\(title) language: \(language)") + PKLog.debug("init:: id:\(String(describing: id)) title:\(String(describing: title)) language: \(String(describing: language))") self.id = id self.title = title diff --git a/Classes/Player/TracksManager.swift b/Classes/Player/TracksManager.swift index a6452bcf..1f7d8cde 100644 --- a/Classes/Player/TracksManager.swift +++ b/Classes/Player/TracksManager.swift @@ -31,7 +31,7 @@ class TracksManager: NSObject { if self.audioTracks != nil || self.textTracks != nil { - PKLog.debug("audio tracks:: \(self.audioTracks), text tracks:: \(self.textTracks)") + PKLog.debug("audio tracks:: \(String(describing: self.audioTracks)), text tracks:: \(String(describing: self.textTracks))") block(PKTracks(audioTracks: self.audioTracks, textTracks: self.textTracks)) } else { PKLog.debug("no audio/ text tracks") diff --git a/Classes/Providers/OTT/PhoenixMediaProvider.swift b/Classes/Providers/OTT/PhoenixMediaProvider.swift index 6033f252..04c8ce62 100644 --- a/Classes/Providers/OTT/PhoenixMediaProvider.swift +++ b/Classes/Providers/OTT/PhoenixMediaProvider.swift @@ -49,12 +49,15 @@ import KalturaNetKit /************************************************************/ // MARK: - PhoenixMediaProviderError /************************************************************/ + public enum PhoenixMediaProviderError: PKError { case invalidInputParam(param: String) case unableToParseData(data: Any) case noSourcesFound case serverError(info:String) + /// in case the response data is empty + case emptyResponse static let domain = "com.kaltura.playkit.error.PhoenixMediaProvider" @@ -64,6 +67,7 @@ public enum PhoenixMediaProviderError: PKError { case .unableToParseData: return 1 case .noSourcesFound: return 2 case .serverError: return 3 + case .emptyResponse: return 4 } } @@ -71,9 +75,10 @@ public enum PhoenixMediaProviderError: PKError { switch self { case .invalidInputParam(let param): return "Invalid input param: \(param)" - case .unableToParseData(let data): return "Unable to parse object" + case .unableToParseData(let data): return "Unable to parse object (data: \(String(describing: data)))" case .noSourcesFound: return "No source found to play content" case .serverError(let info): return "Server Error: \(info)" + case .emptyResponse: return "Response data is empty" } } @@ -290,21 +295,31 @@ public enum PhoenixMediaProviderError: PKError { callback(nil, PhoenixMediaProviderError.invalidInputParam(param:"requests params")) return } - + let isMultiRequest = requestBuilder is KalturaMultiRequestBuilder let request = requestBuilder.set(completion: { (response: Response) in + if let error = response.error { + // if error is of type `PKError` pass it as `NSError` else pass the `Error` object. + callback(nil, (error as? PKError)?.asNSError ?? error) + } + + guard let responseData = response.data else { + callback(nil, PhoenixMediaProviderError.emptyResponse.asNSError) + return + } + var playbackContext: OTTBaseObject? = nil do { if (isMultiRequest) { - playbackContext = try OTTMultiResponseParser.parse(data: response.data).last + playbackContext = try OTTMultiResponseParser.parse(data: responseData).last } else { - playbackContext = try OTTResponseParser.parse(data: response.data) + playbackContext = try OTTResponseParser.parse(data: responseData) } } catch { - callback(nil, PhoenixMediaProviderError.unableToParseData(data:response.data).asNSError) + callback(nil, PhoenixMediaProviderError.unableToParseData(data: responseData).asNSError) } if let context = playbackContext as? OTTPlaybackContext { @@ -315,9 +330,9 @@ public enum PhoenixMediaProviderError: PKError { callback(nil, PhoenixMediaProviderError.noSourcesFound.asNSError) } } else if let error = playbackContext as? OTTError { - callback(nil, PhoenixMediaProviderError.serverError(info: error.message ?? "Unknown Error").asNSError) + callback(nil, PhoenixMediaProviderError.serverError(info: error.message ?? "Unknown Error").asNSError) } else { - callback(nil, PhoenixMediaProviderError.unableToParseData(data: response.data).asNSError) + callback(nil, PhoenixMediaProviderError.unableToParseData(data: responseData).asNSError) } }).build() diff --git a/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift b/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift index 173ac5e9..f80e6480 100644 --- a/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift +++ b/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift @@ -63,7 +63,7 @@ public class PhoenixAnalyticsPlugin: BaseOTTAnalyticsPlugin { requestBuilder.set { (response: Response) in PKLog.trace("Response: \(response)") if response.statusCode == 0 { - PKLog.trace("\(response.data)") + PKLog.trace("\(String(describing: response.data))") guard let data = response.data as? [String: Any] else { return } guard let result = data["result"] as? [String: Any] else { return } guard let errorData = result["error"] as? [String: Any] else { return } diff --git a/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift b/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift index 372f0015..629a6baf 100644 --- a/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift +++ b/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift @@ -59,7 +59,7 @@ public class TVPAPIAnalyticsPlugin: BaseOTTAnalyticsPlugin { requestBuilder.set { (response: Response) in PKLog.trace("Response: \(response)") if response.statusCode == 0 { - PKLog.trace("\(response.data)") + PKLog.trace("\(String(describing: response.data))") guard let data = response.data as? String, data.lowercased() == "\"concurrent\"" else { return } self.reportConcurrencyEvent() } From d5060ad00677cfcd8a10d6f68d36ae3151c0c314 Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Mon, 8 May 2017 10:35:17 +0300 Subject: [PATCH 07/13] #FEM-1388 (#157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed issue where ‘KalturaLiveStreamEntry’ wasn’t parsed. Added new OVPLiveStreamEntry. --- .../OVP/Model/OVPLiveStreamEntry.swift | 24 +++++++++++++++++++ .../Backend/OVP/Parsers/OVPObjectMapper.swift | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 Classes/Backend/OVP/Model/OVPLiveStreamEntry.swift diff --git a/Classes/Backend/OVP/Model/OVPLiveStreamEntry.swift b/Classes/Backend/OVP/Model/OVPLiveStreamEntry.swift new file mode 100644 index 00000000..ea97dfd8 --- /dev/null +++ b/Classes/Backend/OVP/Model/OVPLiveStreamEntry.swift @@ -0,0 +1,24 @@ +// +// KalturaLiveStreamEntry.swift +// Pods +// +// Created by Gal Orlanczyk on 07/05/2017. +// +// + +import Foundation +import SwiftyJSON + +class OVPLiveStreamEntry: OVPEntry { + + var dvrStatus: Bool? + + let dvrStatusKey = "dvrStatus" + + required init?(json: Any) { + super.init(json: json) + + let jsonObject = JSON(json) + self.dvrStatus = jsonObject[dvrStatusKey].bool + } +} diff --git a/Classes/Backend/OVP/Parsers/OVPObjectMapper.swift b/Classes/Backend/OVP/Parsers/OVPObjectMapper.swift index 230f6b7b..0ad39424 100644 --- a/Classes/Backend/OVP/Parsers/OVPObjectMapper.swift +++ b/Classes/Backend/OVP/Parsers/OVPObjectMapper.swift @@ -27,6 +27,8 @@ class OVPObjectMapper: NSObject { switch name { case "KalturaMediaEntry": return OVPEntry.self + case "KalturaLiveStreamEntry": + return OVPLiveStreamEntry.self case "KalturaPlaybackContext": return OVPPlaybackContext.self case "KalturaAPIException": From b00632cd50933e87eaf4afb584617fe93280c5af Mon Sep 17 00:00:00 2001 From: Noam Tamim Date: Mon, 8 May 2017 13:06:16 +0300 Subject: [PATCH 08/13] Fixed getPreferredDownloadableMediaSource (#158) A source with drmParams != nil but empty should be considered clear. --- Classes/Managers/LocalAssetsManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Managers/LocalAssetsManager.swift b/Classes/Managers/LocalAssetsManager.swift index cde23acf..b66acf88 100644 --- a/Classes/Managers/LocalAssetsManager.swift +++ b/Classes/Managers/LocalAssetsManager.swift @@ -109,7 +109,7 @@ import AVFoundation return source } } else { - if let source = sources.first(where: {$0.fileExt=="m3u8" && $0.drmData==nil}) { + if let source = sources.first(where: {$0.fileExt=="m3u8" && ($0.drmData == nil || $0.drmData!.isEmpty)}) { return source } } From 88fea069d570d8fb46b7753a73c2fc0eee6f1588 Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Tue, 9 May 2017 15:03:06 +0300 Subject: [PATCH 09/13] #FEM-1385 (#156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #FEM-1385 * updated play session id to have 2 guid in the following format: ‘player-guid:media-guid’ * * Fixed issues with session id adapter param updating. * * Fixed issue with session id updating --- .../KalturaPlaybackRequestAdapter.swift | 16 +++----- Classes/PKRequestParams.swift | 1 + Classes/Player/Player.swift | 6 +-- Classes/Player/PlayerController.swift | 38 +++++++++++++------ Classes/Player/PlayerDecoratorBase.swift | 13 +------ Classes/Player/PlayerLoader.swift | 2 + .../KalturaLiveStatsPlugin.swift | 2 +- Plugins/KalturaStats/KalturaStatsPlugin.swift | 2 +- 8 files changed, 41 insertions(+), 39 deletions(-) diff --git a/Classes/Network/KalturaPlaybackRequestAdapter.swift b/Classes/Network/KalturaPlaybackRequestAdapter.swift index 57ebe68d..3e219f1f 100644 --- a/Classes/Network/KalturaPlaybackRequestAdapter.swift +++ b/Classes/Network/KalturaPlaybackRequestAdapter.swift @@ -10,22 +10,18 @@ import Foundation class KalturaPlaybackRequestAdapter: PKRequestParamsAdapter { - private var playSessionId: UUID + private var sessionId: String? - init(playSessionId: UUID) { - self.playSessionId = playSessionId + func updateRequestAdapter(withPlayer player: Player) { + self.sessionId = player.sessionId } - public static func setup(player: Player) { - let adapter = KalturaPlaybackRequestAdapter(playSessionId: player.sessionId) - player.settings.set(contentRequestAdapter: adapter) - } - - public func adapt(requestParams: PKRequestParams) -> PKRequestParams { + func adapt(requestParams: PKRequestParams) -> PKRequestParams { + guard let sessionId = self.sessionId else { return requestParams } guard requestParams.url.path.contains("/playManifest/") else { return requestParams } guard var urlComponents = URLComponents(url: requestParams.url, resolvingAgainstBaseURL: false) else { return requestParams } // add query items to the request - let queryItems = [URLQueryItem(name: "playSessionId", value: self.playSessionId.uuidString), URLQueryItem(name: "clientTag", value: PlayKitManager.clientTag)] + let queryItems = [URLQueryItem(name: "playSessionId", value: sessionId), URLQueryItem(name: "clientTag", value: PlayKitManager.clientTag)] if var urlQueryItems = urlComponents.queryItems { urlQueryItems += queryItems urlComponents.queryItems = urlQueryItems diff --git a/Classes/PKRequestParams.swift b/Classes/PKRequestParams.swift index 6c9e3f36..84671921 100644 --- a/Classes/PKRequestParams.swift +++ b/Classes/PKRequestParams.swift @@ -10,6 +10,7 @@ import Foundation /// `PKRequestParamsDecorator` used for getting updated request info @objc public protocol PKRequestParamsAdapter { + func updateRequestAdapter(withPlayer player: Player) func adapt(requestParams: PKRequestParams) -> PKRequestParams } diff --git a/Classes/Player/Player.swift b/Classes/Player/Player.swift index ff320161..2b9d6097 100644 --- a/Classes/Player/Player.swift +++ b/Classes/Player/Player.swift @@ -16,10 +16,10 @@ import AVKit /// `PlayerSettings` used for optional `Player` settings. @objc public protocol PlayerSettings { - func set(contentRequestAdapter: PKRequestParamsAdapter?) + var contentRequestAdapter: PKRequestParamsAdapter? { get set } } -@objc public protocol Player: PlayerSettings { +@objc public protocol Player { @objc weak var delegate: PlayerDelegate? { get set } @@ -48,7 +48,7 @@ import AVKit @objc var currentTextTrack: String? { get } /// The player's session id. the `sessionId` is initialized when the player loads. - @objc var sessionId: UUID { get } + @objc var sessionId: String { get } /// Prepare for playing an entry. play when it's ready. (preparing starts buffering the entry) @objc func prepare(_ config: MediaConfig) diff --git a/Classes/Player/PlayerController.swift b/Classes/Player/PlayerController.swift index 4d8740ef..2c12ad8c 100644 --- a/Classes/Player/PlayerController.swift +++ b/Classes/Player/PlayerController.swift @@ -10,7 +10,7 @@ import Foundation import AVFoundation import AVKit -class PlayerController: NSObject, Player { +class PlayerController: NSObject, Player, PlayerSettings { var onEventBlock: ((PKEvent)->Void)? @@ -18,7 +18,8 @@ class PlayerController: NSObject, Player { fileprivate var currentPlayer: AVPlayerEngine fileprivate var assetBuilder: AssetBuilder? - fileprivate var contentRequestAdapter: PKRequestParamsAdapter? + + var contentRequestAdapter: PKRequestParamsAdapter? var settings: PlayerSettings { return self @@ -53,12 +54,17 @@ class PlayerController: NSObject, Player { return self.currentPlayer.view } - public let sessionId = UUID() + public var sessionId: String { + return self.sessionUUID.uuidString + ":" + (self.mediaSessionUUID?.uuidString ?? "") + } + + let sessionUUID = UUID() + var mediaSessionUUID: UUID? public override init() { self.currentPlayer = AVPlayerEngine() - self.contentRequestAdapter = KalturaPlaybackRequestAdapter(playSessionId: self.sessionId) super.init() + self.currentPlayer.onEventBlock = { [weak self] event in PKLog.trace("postEvent:: \(event)") self?.onEventBlock?(event) @@ -71,10 +77,9 @@ class PlayerController: NSObject, Player { var shouldRefresh: Bool = false func prepare(_ config: MediaConfig) { - // configure media sources content request adapter - if let contentRequestAdapter = self.contentRequestAdapter { - config.mediaEntry.configureMediaSource(withContentRequestAdapter: contentRequestAdapter) - } + // update the media source request adapter with new media uuid if using kaltura request adapter + self.updateRequestAdapterIfExists(forMediaConfig: config) + self.currentPlayer.startPosition = config.startTime self.assetBuilder = AssetBuilder(mediaEntry: config.mediaEntry) self.assetBuilder?.build { (error: Error?, asset: AVAsset?) in @@ -135,13 +140,22 @@ class PlayerController: NSObject, Player { } /************************************************************/ -// MARK: - PlayerSettings +// MARK: - Private /************************************************************/ -extension PlayerController: PlayerSettings { +extension PlayerController { - func set(contentRequestAdapter: PKRequestParamsAdapter?) { - self.contentRequestAdapter = contentRequestAdapter + /// Updates the request adapter if it is kaltura type + fileprivate func updateRequestAdapterIfExists(forMediaConfig config: MediaConfig) { + // configure media sources content request adapter if kaltura request adapter exists + if let _ = self.contentRequestAdapter as? KalturaPlaybackRequestAdapter { + // create new media session uuid + self.mediaSessionUUID = UUID() + // update the request adapter with the updated session id + self.contentRequestAdapter!.updateRequestAdapter(withPlayer: self) + // configure media source with the adapter + config.mediaEntry.configureMediaSource(withContentRequestAdapter: self.contentRequestAdapter!) + } } } diff --git a/Classes/Player/PlayerDecoratorBase.swift b/Classes/Player/PlayerDecoratorBase.swift index 6ed90242..da46d7ad 100644 --- a/Classes/Player/PlayerDecoratorBase.swift +++ b/Classes/Player/PlayerDecoratorBase.swift @@ -60,7 +60,7 @@ import AVKit return self.player.view } - public var sessionId: UUID { + public var sessionId: String { return self.player.sessionId } @@ -122,14 +122,3 @@ import AVKit } } -/************************************************************/ -// MARK: - PlayerSettings -/************************************************************/ - -extension PlayerDecoratorBase: PlayerSettings { - - public func set(contentRequestAdapter: PKRequestParamsAdapter?) { - self.player.set(contentRequestAdapter: contentRequestAdapter) - } -} - diff --git a/Classes/Player/PlayerLoader.swift b/Classes/Player/PlayerLoader.swift index d5578e40..91db2f8e 100644 --- a/Classes/Player/PlayerLoader.swift +++ b/Classes/Player/PlayerLoader.swift @@ -31,6 +31,8 @@ class PlayerLoader: PlayerDecoratorBase { } var player: Player = playerController + // initial creation of play session id adapter will update session id in prepare if needed + player.settings.contentRequestAdapter = KalturaPlaybackRequestAdapter() if let pluginConfigs = pluginConfig?.config { for pluginName in pluginConfigs.keys { diff --git a/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift b/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift index 1cca77bf..21772ee7 100644 --- a/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift +++ b/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift @@ -205,7 +205,7 @@ public class KalturaLiveStatsPlugin: BaseAnalyticsPlugin { self.messageBus?.post(event) let entryId: String - let sessionId = player.sessionId.uuidString + let sessionId = player.sessionId var baseUrl = "https://stats.kaltura.com/api_v3/index.php" var parterId = "" diff --git a/Plugins/KalturaStats/KalturaStatsPlugin.swift b/Plugins/KalturaStats/KalturaStatsPlugin.swift index 840f0fb3..80b2bb5c 100644 --- a/Plugins/KalturaStats/KalturaStatsPlugin.swift +++ b/Plugins/KalturaStats/KalturaStatsPlugin.swift @@ -299,7 +299,7 @@ public class KalturaStatsPlugin: BaseAnalyticsPlugin { PKLog.debug("Action: \(action)") let entryId: String - let sessionId = player.sessionId.uuidString + let sessionId = player.sessionId var baseUrl = "https://stats.kaltura.com/api_v3/index.php" var confId = 0 var parterId = "" From 04b537cc1e96d5171638f73fc007f947e1a151eb Mon Sep 17 00:00:00 2001 From: ElizaSapir Date: Wed, 10 May 2017 12:27:28 +0300 Subject: [PATCH 10/13] FEM-1394 #comment avoid sending stop after finish - analytics (#159) --- Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift index d152c637..fc1c43bb 100644 --- a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift +++ b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift @@ -15,6 +15,7 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt var intervalOn: Bool = false var timer: Timer? var interval: TimeInterval = 30 + var isContentEnded: Bool = false /************************************************************/ // MARK: - PKPlugin @@ -28,13 +29,14 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt public override func onUpdateMedia(mediaConfig: MediaConfig) { super.onUpdateMedia(mediaConfig: mediaConfig) self.intervalOn = false + self.isContentEnded = false self.timer?.invalidate() } public override func destroy() { super.destroy() - // only send stop event if content started playing already - if !self.isFirstPlay { + // 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() @@ -65,7 +67,8 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt PlayerEvent.error, PlayerEvent.pause, PlayerEvent.loadedMetadata, - PlayerEvent.playing + PlayerEvent.playing, + PlayerEvent.seeked ] } @@ -76,12 +79,14 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt PKLog.debug("Register event: \(event.self)") switch event { + case let e where e.self == PlayerEvent.seeked: strongSelf.isContentEnded = false 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 } case let e where e.self == PlayerEvent.error: self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in From 4fcefc7543ec8291be4097f28d83c6da9dd347ab Mon Sep 17 00:00:00 2001 From: ElizaSapir Date: Wed, 10 May 2017 14:00:38 +0300 Subject: [PATCH 11/13] =?UTF-8?q?FEM-1381=20#comment=20avoid=20dealloc=20w?= =?UTF-8?q?hile=20key=20value=20observers=20were=20still=20=E2=80=A6=20(#1?= =?UTF-8?q?55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * FEM-1381 #comment avoid dealloc while key value observers were still registered * FEM-1381 #comment update ima sdk version * FEM-1381 #comment remove dynamic ad insertion event handling --- Classes/Player/AVPlayerEngine/AVPlayerEngine.swift | 2 ++ Classes/PlayerEvent.swift | 8 +------- PlayKit.podspec | 2 +- Plugins/IMA/IMAPlugin.swift | 10 +++------- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift index c694a23b..3d823628 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift @@ -159,6 +159,8 @@ class AVPlayerEngine: AVPlayer { deinit { PKLog.debug("\(String(describing: type(of: self))), was deinitialized") + // Avoid dealloc while key value observers were still registered + self.removeObservers() } func stop() { diff --git a/Classes/PlayerEvent.swift b/Classes/PlayerEvent.swift index dde9b4f7..61925c93 100644 --- a/Classes/PlayerEvent.swift +++ b/Classes/PlayerEvent.swift @@ -133,12 +133,10 @@ import AVFoundation @objc public class AdEvent: PKEvent { @objc public static let allEventTypes: [AdEvent.Type] = [ - adBreakReady, adBreakEnded, adBreakStarted, allAdsCompleted, adComplete, adClicked, adCuePointsUpdate, adFirstQuartile, adLoaded, adLog, adMidpoint, adPaused, adResumed, adSkipped, adStarted, adStreamLoaded, adTapped, adThirdQuartile, adDidProgressToTime, adDidRequestPause, adDidRequestResume, adWebOpenerWillOpenExternalBrowser, adWebOpenerWillOpenInAppBrowser, adWebOpenerDidOpenInAppBrowser, adWebOpenerWillCloseInAppBrowser, adWebOpenerDidCloseInAppBrowser, requestTimedOut + adBreakReady, allAdsCompleted, adComplete, adClicked, adCuePointsUpdate, adFirstQuartile, adLoaded, adLog, adMidpoint, adPaused, adResumed, adSkipped, adStarted, adTapped, adThirdQuartile, adDidProgressToTime, adDidRequestPause, adDidRequestResume, adWebOpenerWillOpenExternalBrowser, adWebOpenerWillOpenInAppBrowser, adWebOpenerDidOpenInAppBrowser, adWebOpenerWillCloseInAppBrowser, adWebOpenerDidCloseInAppBrowser, requestTimedOut ] @objc public static let adBreakReady: AdEvent.Type = AdBreakReady.self - @objc public static let adBreakEnded: AdEvent.Type = AdBreakEnded.self - @objc public static let adBreakStarted: AdEvent.Type = AdBreakStarted.self @objc public static let allAdsCompleted: AdEvent.Type = AllAdsCompleted.self @objc public static let adComplete: AdEvent.Type = AdComplete.self @objc public static let adClicked: AdEvent.Type = AdClicked.self @@ -150,7 +148,6 @@ import AVFoundation @objc public static let adResumed: AdEvent.Type = AdResumed.self @objc public static let adSkipped: AdEvent.Type = AdSkipped.self @objc public static let adStarted: AdEvent.Type = AdStarted.self - @objc public static let adStreamLoaded: AdEvent.Type = AdStreamLoaded.self @objc public static let adTapped: AdEvent.Type = AdTapped.self @objc public static let adThirdQuartile: AdEvent.Type = AdThirdQuartile.self @objc public static let adDidProgressToTime: AdEvent.Type = AdDidProgressToTime.self @@ -181,8 +178,6 @@ import AVFoundation } class AdBreakReady: AdEvent {} - class AdBreakEnded: AdEvent {} - class AdBreakStarted: AdEvent {} class AllAdsCompleted: AdEvent {} class AdComplete: AdEvent {} class AdClicked: AdEvent {} @@ -192,7 +187,6 @@ import AVFoundation class AdPaused: AdEvent {} class AdResumed: AdEvent {} class AdSkipped: AdEvent {} - class AdStreamLoaded: AdEvent {} class AdTapped: AdEvent {} class AdThirdQuartile: AdEvent {} diff --git a/PlayKit.podspec b/PlayKit.podspec index 095c366e..eb5b433b 100644 --- a/PlayKit.podspec +++ b/PlayKit.podspec @@ -28,7 +28,7 @@ s.subspec 'IMAPlugin' do |ssp| 'LIBRARY_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**' } ssp.dependency 'PlayKit/Core' - ssp.dependency 'GoogleAds-IMA-iOS-SDK', '3.4.1' + ssp.dependency 'GoogleAds-IMA-iOS-SDK', '3.5.2' end s.subspec 'GoogleCastAddon' do |ssp| diff --git a/Plugins/IMA/IMAPlugin.swift b/Plugins/IMA/IMAPlugin.swift index 8d7eb6e5..b29e8b1e 100644 --- a/Plugins/IMA/IMAPlugin.swift +++ b/Plugins/IMA/IMAPlugin.swift @@ -291,10 +291,6 @@ enum IMAState: Int, StateProtocol { let event = event.ad != nil ? AdEvent.AdStarted(adInfo: PKAdInfo(ad: event.ad)) : AdEvent.AdStarted() self.notify(event: event) self.showLoadingView(false, alpha: 0) - case .AD_BREAK_STARTED: - self.notify(event: AdEvent.AdBreakStarted()) - self.showLoadingView(false, alpha: 0) - case .AD_BREAK_ENDED: self.notify(event: AdEvent.AdBreakEnded()) case .ALL_ADS_COMPLETED: // detaching the delegate and destroying the adsManager. // means all ads have been played so we can destroy the adsManager. @@ -302,16 +298,16 @@ enum IMAState: Int, StateProtocol { self.notify(event: AdEvent.AllAdsCompleted()) case .CLICKED: self.notify(event: AdEvent.AdClicked()) case .COMPLETE: self.notify(event: AdEvent.AdComplete()) - case .CUEPOINTS_CHANGED: self.notify(event: AdEvent.AdCuePointsUpdate(adCuePoints: adsManager.getAdCuePoints())) case .FIRST_QUARTILE: self.notify(event: AdEvent.AdFirstQuartile()) case .LOG: self.notify(event: AdEvent.AdLog()) case .MIDPOINT: self.notify(event: AdEvent.AdMidpoint()) case .PAUSE: self.notify(event: AdEvent.AdPaused()) case .RESUME: self.notify(event: AdEvent.AdResumed()) case .SKIPPED: self.notify(event: AdEvent.AdSkipped()) - case .STREAM_LOADED: self.notify(event: AdEvent.AdStreamLoaded()) case .TAPPED: self.notify(event: AdEvent.AdTapped()) case .THIRD_QUARTILE: self.notify(event: AdEvent.AdThirdQuartile()) + // Only used for dynamic ad insertion (not officially supported) + case .AD_BREAK_ENDED, .AD_BREAK_STARTED, .CUEPOINTS_CHANGED, .STREAM_LOADED, .STREAM_STARTED: break } } @@ -454,7 +450,7 @@ enum IMAState: Int, StateProtocol { } private func shouldDiscardAd() -> Bool { - if currentTime < self.dataSource?.adsPluginStartTime ?? 0 { + if self.currentTime < self.dataSource?.adsPluginStartTime ?? 0 { return true } return false From 317538c32038e5da2d4e7ddb69228855e6c67b7b Mon Sep 17 00:00:00 2001 From: ElizaSapir Date: Wed, 10 May 2017 14:21:58 +0300 Subject: [PATCH 12/13] FEM-1394 #comment fix strongSelf error (#160) --- Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift index fc1c43bb..0a8a4255 100644 --- a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift +++ b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift @@ -79,7 +79,7 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt PKLog.debug("Register event: \(event.self)") switch event { - case let e where e.self == PlayerEvent.seeked: strongSelf.isContentEnded = false + case let e where e.self == PlayerEvent.seeked: self.isContentEnded = false 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 } From bd2eacb9583422c9d7d1025f6280b101119afc4d Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Wed, 10 May 2017 15:20:36 +0300 Subject: [PATCH 13/13] FEM-1270 SmartAds (#148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial adnalyzer for youbora smart ads implementation * * Added events posting for ad buffering, ad playback ready and ads requested. * Added handling for new events posting in AdnalyzerManager. * Fixed some issues with adnalyzer manager. * * Added `Stopped` and `LoadingAsset` event. * Added endedHandler for youbora plugin. * Moved `YouboraPluginError` to it’s own class. * Small fixes * * Fixed issue with first play * Fixed issue where /adStart could be received before /start in youbora * * enabled adnalyzer to always be on. * * Removed `LoadingAsset` event (not needed). * Fixed issue where we have post roll and we receive ended before the ad ended in youbora. * Remove unneeded code for youbora setup. * * Added enable adnalyzer flag * * Small fix to youbora adnalyzer implementation. * * renamed `AdDidRequestResume` to `AdDidRequestContentResume` and `AdDidRequestPause` to `AdDidRequestContentPause`. * * Fixed plugin version for youbora. * Improved youbora plugin readability by dividing the code better between the components. * * Added missing info methods for youbora manager. * Renamed `PlaybackParamsUpdated` to `PlaybackInfo` and associated `PKPlaybackInfo` object containing all bitrate params. * Fixed issue with youbora entering background now stops the events and starts again when returning. * Renamed ‘enableAdnalyzer’ to ‘enableSmartAds’ for better readability. * * Fixed `PlaybackInfo` naming from old name(PlaybackParamsUpdated). * * Fixed codacy issues * * Added error handling for youbora. * Paused ads player when going to background * Fixed youbora plugin version string * * Fixed issues with PR --- Classes/PKError.swift | 10 +- .../AVPlayerEngine+AssetLoading.swift | 3 + .../AVPlayerEngine+Observation.swift | 34 +- .../AVPlayerEngine/AVPlayerEngine.swift | 1 + Classes/Player/PKEvent.swift | 6 +- Classes/PlayerEvent.swift | 78 +++- Plugins/IMA/AdsEnabledPlayerController.swift | 26 +- Plugins/IMA/IMAPlugin.swift | 16 +- .../KalturaLiveStatsPlugin.swift | 8 +- Plugins/Youbora/YouboraAdnalyzerManager.swift | 213 +++++++++++ Plugins/Youbora/YouboraManager.swift | 282 +++++++++++++-- Plugins/Youbora/YouboraPlugin.swift | 338 +++++++----------- Plugins/Youbora/YouboraPluginError.swift | 43 +++ 13 files changed, 764 insertions(+), 294 deletions(-) create mode 100644 Plugins/Youbora/YouboraAdnalyzerManager.swift create mode 100644 Plugins/Youbora/YouboraPluginError.swift diff --git a/Classes/PKError.swift b/Classes/PKError.swift index 992849cc..143ac666 100644 --- a/Classes/PKError.swift +++ b/Classes/PKError.swift @@ -18,7 +18,7 @@ enum PlayerError: PKError { case failedToLoadAssetFromKeys(rootError: NSError?) case assetNotPlayable - case failedToPlayToEndTime(rootError: NSError) + case playerItemFailed(rootError: NSError) static let domain = "com.kaltura.playkit.error.player" @@ -26,7 +26,7 @@ enum PlayerError: PKError { switch self { case .failedToLoadAssetFromKeys: return PKErrorCode.failedToLoadAssetFromKeys case .assetNotPlayable: return PKErrorCode.assetNotPlayable - case .failedToPlayToEndTime: return PKErrorCode.failedToPlayToEndTime + case .playerItemFailed: return PKErrorCode.playerItemFailed } } @@ -34,7 +34,7 @@ enum PlayerError: PKError { switch self { case .failedToLoadAssetFromKeys: return "Can't use this AVAsset because one of it's keys failed to load" case .assetNotPlayable: return "Can't use this AVAsset because it isn't playable" - case .failedToPlayToEndTime: return "Item failed to play to its end time" + case .playerItemFailed: return "Player item failed to play" } } @@ -46,7 +46,7 @@ enum PlayerError: PKError { } return [:] case .assetNotPlayable: return [:] - case .failedToPlayToEndTime(let rootError): return [PKErrorKeys.RootErrorKey: rootError] + case .playerItemFailed(let rootError): return [PKErrorKeys.RootErrorKey: rootError] } } } @@ -236,7 +236,7 @@ struct PKErrorKeys { // PlayerError @objc(FailedToLoadAssetFromKeys) public static let failedToLoadAssetFromKeys = 7000 @objc(AssetNotPlayable) public static let assetNotPlayable = 7001 - @objc(FailedToPlayToEndTime) public static let failedToPlayToEndTime = 7002 + @objc(PlayerItemFailed) public static let playerItemFailed = 7002 // PlayerErrorLog @objc(PlayerItemErrorLogEvent) public static let playerItemErrorLogEvent = 7100 // PKPluginError diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift index ccee3b86..22226ca2 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift @@ -12,8 +12,11 @@ import AVFoundation extension AVPlayerEngine { override func replaceCurrentItem(with item: AVPlayerItem?) { + // when changing asset reset last timebase + self.lastTimebaseRate = 0 // When changing media (loading new asset) we want to reset isFirstReady in order to receive `CanPlay` & `LoadedMetadata` accuratly. self.isFirstReady = true + super.replaceCurrentItem(with: item) } diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift index 58076641..3e3080d9 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift @@ -35,7 +35,7 @@ extension AVPlayerEngine { } NotificationCenter.default.addObserver(self, selector: #selector(self.playerFailed(notification:)), name: .AVPlayerItemFailedToPlayToEndTime, object: self.currentItem) - NotificationCenter.default.addObserver(self, selector: #selector(self.playerPlayedToEnd(notification:)), name: .AVPlayerItemDidPlayToEndTime, object: self.currentItem) + NotificationCenter.default.addObserver(self, selector: #selector(self.playerPlayedToEnd(notification:)), name: .AVPlayerItemDidPlayToEndTime, object: self.currentItem) // TODO: check if fired same as playerItem.status == failed if yes then remove this notificaiton observation. NotificationCenter.default.addObserver(self, selector: #selector(self.onAccessLogEntryNotification), name: .AVPlayerItemNewAccessLogEntry, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.onErrorLogEntryNotification), name: .AVPlayerItemNewErrorLogEntry, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.timebaseChanged), name: Notification.Name(kCMTimebaseNotification_EffectiveRateChanged as String), object: self.currentItem?.timebase) @@ -66,11 +66,7 @@ extension AVPlayerEngine { PKLog.debug("event log:\n event log: averageAudioBitrate - \(lastEvent.averageAudioBitrate)\n event log: averageVideoBitrate - \(lastEvent.averageVideoBitrate)\n event log: indicatedAverageBitrate - \(lastEvent.indicatedAverageBitrate)\n event log: indicatedBitrate - \(lastEvent.indicatedBitrate)\n event log: observedBitrate - \(lastEvent.observedBitrate)\n event log: observedMaxBitrate - \(lastEvent.observedMaxBitrate)\n event log: observedMinBitrate - \(lastEvent.observedMinBitrate)\n event log: switchBitrate - \(lastEvent.switchBitrate)") } - if lastEvent.indicatedBitrate != self.lastBitrate { - self.lastBitrate = lastEvent.indicatedBitrate - PKLog.trace("currentBitrate:: \(self.lastBitrate)") - self.post(event: PlayerEvent.PlaybackParamsUpdated(currentBitrate: self.lastBitrate)) - } + self.post(event: PlayerEvent.PlaybackInfo(playbackInfo: PKPlaybackInfo(logEvent: lastEvent))) } } @@ -86,7 +82,7 @@ extension AVPlayerEngine { self.currentState = newState if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? NSError { - self.post(event: PlayerEvent.Error(error: PlayerError.failedToPlayToEndTime(rootError: error))) + self.post(event: PlayerEvent.Error(error: PlayerError.playerItemFailed(rootError: error))) } else { self.post(event: PlayerEvent.Error()) } @@ -119,20 +115,13 @@ extension AVPlayerEngine { PKLog.debug("keyPath:: \(keyPath)") switch keyPath { - case #keyPath(currentItem.playbackLikelyToKeepUp): - self.handleLikelyToKeepUp() - case #keyPath(currentItem.playbackBufferEmpty): - self.handleBufferEmptyChange() - case #keyPath(rate): - self.handleRate() - case #keyPath(currentItem.status): - self.handleStatusChange() - case #keyPath(currentItem): - self.handleItemChange() - case #keyPath(currentItem.timedMetadata): - self.handleTimedMedia() - default: - super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + case #keyPath(currentItem.playbackLikelyToKeepUp): self.handleLikelyToKeepUp() + case #keyPath(currentItem.playbackBufferEmpty): self.handleBufferEmptyChange() + case #keyPath(rate): self.handleRate() + case #keyPath(currentItem.status): self.handleStatusChange() + case #keyPath(currentItem): self.handleItemChange() + case #keyPath(currentItem.timedMetadata): self.handleTimedMedia() + default: super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } } @@ -205,6 +194,9 @@ extension AVPlayerEngine { let newState = PlayerState.error self.postStateChange(newState: newState, oldState: self.currentState) self.currentState = newState + if let error = currentItem?.error as NSError? { + self.post(event: PlayerEvent.Error(error: PlayerError.playerItemFailed(rootError: error))) + } } } diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift index 3d823628..e43d764a 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift @@ -168,6 +168,7 @@ class AVPlayerEngine: AVPlayer { self.pause() self.seek(to: kCMTimeZero) self.replaceCurrentItem(with: nil) + self.post(event: PlayerEvent.Stopped()) } override func pause() { diff --git a/Classes/Player/PKEvent.swift b/Classes/Player/PKEvent.swift index 5edcc3e2..e40bd12d 100644 --- a/Classes/Player/PKEvent.swift +++ b/Classes/Player/PKEvent.swift @@ -25,7 +25,7 @@ extension PKEvent { struct EventDataKeys { static let Duration = "duration" static let Tracks = "tracks" - static let CurrentBitrate = "currentBitrate" + static let PlaybackInfo = "playbackInfo" static let OldState = "oldState" static let NewState = "newState" static let Error = "error" @@ -45,8 +45,8 @@ extension PKEvent { } /// Current Bitrate Value, PKEvent Data Accessor - @objc public var currentBitrate: NSNumber? { - return self.data?[EventDataKeys.CurrentBitrate] as? NSNumber + @objc public var playbackInfo: PKPlaybackInfo? { + return self.data?[EventDataKeys.PlaybackInfo] as? PKPlaybackInfo } /// Current Old State Value, PKEvent Data Accessor diff --git a/Classes/PlayerEvent.swift b/Classes/PlayerEvent.swift index 61925c93..33ada4e2 100644 --- a/Classes/PlayerEvent.swift +++ b/Classes/PlayerEvent.swift @@ -8,6 +8,32 @@ import Foundation import AVFoundation +@objc public class PKPlaybackInfo: NSObject { + let bitrate: Double + let indicatedBitrate: Double + let observedBitrate: Double + + init(bitrate: Double, indicatedBitrate: Double, observedBitrate: Double) { + self.bitrate = bitrate + self.indicatedBitrate = indicatedBitrate + self.observedBitrate = observedBitrate + } + + convenience init(logEvent: AVPlayerItemAccessLogEvent) { + let bitrate: Double + if logEvent.segmentsDownloadedDuration > 0 { + // bitrate is equal to: + // (amount of bytes transfered) * 8 (bits in byte) / (amount of time took to download the transfered bytes) + bitrate = Double(logEvent.numberOfBytesTransferred * 8) / logEvent.segmentsDownloadedDuration + } else { + bitrate = logEvent.indicatedBitrate + } + let indicatedBitrate = logEvent.indicatedBitrate + let observedBitrate = logEvent.observedBitrate + self.init(bitrate: bitrate, indicatedBitrate: indicatedBitrate, observedBitrate: observedBitrate) + } +} + /// PlayerEvent is a class used to reflect player events. @objc public class PlayerEvent: PKEvent { @@ -15,7 +41,7 @@ import AVFoundation @objc public static let allEventTypes: [PlayerEvent.Type] = [ canPlay, durationChanged, ended, loadedMetadata, play, pause, playing, seeking, seeked, stateChanged, - tracksAvailable, playbackParamsUpdated, error + tracksAvailable, playbackInfo, error ] // MARK: - Player Events Static Reference @@ -24,6 +50,8 @@ import AVFoundation @objc public static let canPlay: PlayerEvent.Type = CanPlay.self /// The metadata has loaded or changed, indicating a change in duration of the media. This is sent, for example, when the media has loaded enough that the duration is known. @objc public static let durationChanged: PlayerEvent.Type = DurationChanged.self + /// Sent when playback stopped. + @objc public static let stopped: PlayerEvent.Type = Stopped.self /// Sent when playback completes. @objc public static let ended: PlayerEvent.Type = Ended.self /// The media's metadata has finished loading; all attributes now contain as much useful information as they're going to. @@ -41,7 +69,7 @@ import AVFoundation /// Sent when tracks available. @objc public static let tracksAvailable: PlayerEvent.Type = TracksAvailable.self /// Sent when Playback Params Updated. - @objc public static let playbackParamsUpdated: PlayerEvent.Type = PlaybackParamsUpdated.self + @objc public static let playbackInfo: PlayerEvent.Type = PlaybackInfo.self /// Sent when player state is changed. @objc public static let stateChanged: PlayerEvent.Type = StateChanged.self /// Sent when timed metadata is available. @@ -63,6 +91,7 @@ import AVFoundation } } + class Stopped: PlayerEvent {} class Ended: PlayerEvent {} class LoadedMetadata: PlayerEvent {} class Play: PlayerEvent {} @@ -115,9 +144,9 @@ import AVFoundation } } - class PlaybackParamsUpdated: PlayerEvent { - convenience init(currentBitrate: Double) { - self.init([EventDataKeys.CurrentBitrate : NSNumber(value: currentBitrate)]) + class PlaybackInfo: PlayerEvent { + convenience init(playbackInfo: PKPlaybackInfo) { + self.init([EventDataKeys.PlaybackInfo: playbackInfo]) } } @@ -133,7 +162,7 @@ import AVFoundation @objc public class AdEvent: PKEvent { @objc public static let allEventTypes: [AdEvent.Type] = [ - adBreakReady, allAdsCompleted, adComplete, adClicked, adCuePointsUpdate, adFirstQuartile, adLoaded, adLog, adMidpoint, adPaused, adResumed, adSkipped, adStarted, adTapped, adThirdQuartile, adDidProgressToTime, adDidRequestPause, adDidRequestResume, adWebOpenerWillOpenExternalBrowser, adWebOpenerWillOpenInAppBrowser, adWebOpenerDidOpenInAppBrowser, adWebOpenerWillCloseInAppBrowser, adWebOpenerDidCloseInAppBrowser, requestTimedOut + adBreakReady, allAdsCompleted, adComplete, adClicked, adCuePointsUpdate, adFirstQuartile, adLoaded, adLog, adMidpoint, adPaused, adResumed, adSkipped, adStarted, adTapped, adThirdQuartile, adDidProgressToTime, adDidRequestContentPause, adDidRequestContentResume, adWebOpenerWillOpenExternalBrowser, adWebOpenerWillOpenInAppBrowser, adWebOpenerDidOpenInAppBrowser, adWebOpenerWillCloseInAppBrowser, adWebOpenerDidCloseInAppBrowser, requestTimedOut ] @objc public static let adBreakReady: AdEvent.Type = AdBreakReady.self @@ -151,8 +180,10 @@ import AVFoundation @objc public static let adTapped: AdEvent.Type = AdTapped.self @objc public static let adThirdQuartile: AdEvent.Type = AdThirdQuartile.self @objc public static let adDidProgressToTime: AdEvent.Type = AdDidProgressToTime.self - @objc public static let adDidRequestPause: AdEvent.Type = AdDidRequestPause.self - @objc public static let adDidRequestResume: AdEvent.Type = AdDidRequestResume.self + /// Ad requested the content to pause (before ad starts playing) + @objc public static let adDidRequestContentPause: AdEvent.Type = AdDidRequestContentPause.self + /// Ad requested content resume (when finished playing ads or when error occurs and playback needs to continue) + @objc public static let adDidRequestContentResume: AdEvent.Type = AdDidRequestContentResume.self @objc public static let webOpenerEvent: AdEvent.Type = WebOpenerEvent.self @objc public static let adWebOpenerWillOpenExternalBrowser: AdEvent.Type = AdWebOpenerWillOpenExternalBrowser.self @objc public static let adWebOpenerWillOpenInAppBrowser: AdEvent.Type = AdWebOpenerWillOpenInAppBrowser.self @@ -160,8 +191,14 @@ import AVFoundation @objc public static let adWebOpenerWillCloseInAppBrowser: AdEvent.Type = AdWebOpenerWillCloseInAppBrowser.self @objc public static let adWebOpenerDidCloseInAppBrowser: AdEvent.Type = AdWebOpenerDidCloseInAppBrowser.self @objc public static let adCuePointsUpdate: AdEvent.Type = AdCuePointsUpdate.self + /// Sent when an ad started buffering + @objc public static let adStartedBuffering: AdEvent.Type = AdStartedBuffering.self + /// Sent when ad finished buffering and ready for playback + @objc public static let adPlaybackReady: AdEvent.Type = AdPlaybackReady.self /// Sent when the ads request timed out. @objc public static let requestTimedOut: AdEvent.Type = RequestTimedOut.self + /// delivered when ads request was sent. + @objc public static let adsRequested: AdEvent.Type = AdsRequested.self /// Sent when an error occurs. @objc public static let error: AdEvent.Type = Error.self @@ -190,6 +227,9 @@ import AVFoundation class AdTapped: AdEvent {} class AdThirdQuartile: AdEvent {} + class AdStartedBuffering: AdEvent {} + class AdPlaybackReady: AdEvent {} + // `AdCuePointsUpdate` event is received when ad cue points were updated. only sent when there is more then 0. class AdCuePointsUpdate: AdEvent { convenience init(adCuePoints: PKAdCuePoints) { @@ -210,11 +250,17 @@ import AVFoundation } } - class AdDidRequestPause: AdEvent {} - class AdDidRequestResume: AdEvent {} + class AdDidRequestContentPause: AdEvent {} + class AdDidRequestContentResume: AdEvent {} /// Sent when the ads request timed out. class RequestTimedOut: AdEvent {} + /// delivered when ads request was sent. + class AdsRequested: AdEvent { + convenience init(adTagUrl: String) { + self.init([AdEventDataKeys.adTagUrl: adTagUrl]) + } + } class WebOpenerEvent: AdEvent { convenience init(webOpener: NSObject!) { @@ -242,22 +288,23 @@ extension PKEvent { static let error = "error" static let adCuePoints = "adCuePoints" static let adInfo = "adInfo" + static let adTagUrl = "adTagUrl" } // MARK: Ad Data Accessors /// MediaTime, PKEvent Ad Data Accessor - @objc public var mediaTime: NSNumber? { + @objc public var adMediaTime: NSNumber? { return self.data?[AdEventDataKeys.mediaTime] as? NSNumber } /// TotalTime, PKEvent Ad Data Accessor - @objc public var totalTime: NSNumber? { + @objc public var adTotalTime: NSNumber? { return self.data?[AdEventDataKeys.totalTime] as? NSNumber } /// WebOpener, PKEvent Ad Data Accessor - @objc public var webOpener: NSObject? { + @objc public var adWebOpener: NSObject? { return self.data?[AdEventDataKeys.webOpener] as? NSObject } @@ -270,4 +317,9 @@ extension PKEvent { @objc public var adCuePoints: PKAdCuePoints? { return self.data?[AdEventDataKeys.adCuePoints] as? PKAdCuePoints } + + /// TotalTime, PKEvent Ad Data Accessor + @objc public var adTagUrl: String? { + return self.data?[AdEventDataKeys.adTagUrl] as? String + } } diff --git a/Plugins/IMA/AdsEnabledPlayerController.swift b/Plugins/IMA/AdsEnabledPlayerController.swift index cc8db43e..7d7e2cbb 100644 --- a/Plugins/IMA/AdsEnabledPlayerController.swift +++ b/Plugins/IMA/AdsEnabledPlayerController.swift @@ -52,6 +52,7 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl init(adsPlugin: AdsPlugin) { super.init() self.adsPlugin = adsPlugin + AppStateSubject.shared.add(observer: self) } override var delegate: PlayerDelegate? { @@ -110,6 +111,11 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl return super.createPiPController(with: self.adsPlugin) } + override func destroy() { + AppStateSubject.shared.remove(observer: self) + super.destroy() + } + /************************************************************/ // MARK: - AdsPluginDataSource /************************************************************/ @@ -142,9 +148,9 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl func adsPlugin(_ adsPlugin: AdsPlugin, didReceive event: PKEvent) { switch event { - case let e where type(of: e) == AdEvent.adDidRequestPause: + case let e where type(of: e) == AdEvent.adDidRequestContentPause: super.pause() - case let e where type(of: e) == AdEvent.adDidRequestResume: + case let e where type(of: e) == AdEvent.adDidRequestContentResume: if !self.shouldPreventContentResume { self.preparePlayerIfNeeded() super.resume() @@ -194,3 +200,19 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl self.prepareSemaphore.signal() } } + +/************************************************************/ +// MARK: - AppStateObservable +/************************************************************/ + +extension AdsEnabledPlayerController: AppStateObservable { + + var observations: Set { + return [ + NotificationObservation(name: .UIApplicationDidEnterBackground) { [unowned self] in + // when we enter background make sure to pause if we were playing. + self.pause() + } + ] + } +} diff --git a/Plugins/IMA/IMAPlugin.swift b/Plugins/IMA/IMAPlugin.swift index b29e8b1e..f57033dd 100644 --- a/Plugins/IMA/IMAPlugin.swift +++ b/Plugins/IMA/IMAPlugin.swift @@ -154,11 +154,15 @@ enum IMAState: Int, StateProtocol { let request = IMAAdsRequest(adTagUrl: self.config.adTagUrl, adDisplayContainer: adDisplayContainer, contentPlayhead: self, userContext: nil) // sets the state to adsRequest self.stateMachine.set(state: .adsRequested) - + // request ads + IMAPlugin.loader.requestAds(with: request) + // notify ads requested + self.notify(event: AdEvent.AdsRequested(adTagUrl: self.config.adTagUrl)) + // start timeout timer self.requestTimeoutTimer = Timer.after(self.requestTimeoutInterval) { [unowned self] in if self.adsManager == nil { self.showLoadingView(false, alpha: 0) - + switch self.stateMachine.getState() { case .adsRequested: self.delegate?.adsRequestTimedOut(shouldPlay: false) case .adsRequestedAndPlay: self.delegate?.adsRequestTimedOut(shouldPlay: true) @@ -172,8 +176,6 @@ enum IMAState: Int, StateProtocol { self.notify(event: AdEvent.RequestTimedOut()) } } - - IMAPlugin.loader.requestAds(with: request) PKLog.trace("request Ads") } @@ -253,10 +255,12 @@ enum IMAState: Int, StateProtocol { public func adsManagerAdDidStartBuffering(_ adsManager: IMAAdsManager!) { self.showLoadingView(true, alpha: 0.1) + self.notify(event: AdEvent.AdStartedBuffering()) } public func adsManagerAdPlaybackReady(_ adsManager: IMAAdsManager!) { self.showLoadingView(false, alpha: 0) + self.notify(event: AdEvent.AdPlaybackReady()) } public func adsManager(_ adsManager: IMAAdsManager!, didReceive event: IMAAdEvent!) { @@ -320,13 +324,13 @@ enum IMAState: Int, StateProtocol { public func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager!) { self.stateMachine.set(state: .adsPlaying) - self.notify(event: AdEvent.AdDidRequestPause()) + self.notify(event: AdEvent.AdDidRequestContentPause()) } public func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager!) { self.stateMachine.set(state: .contentPlaying) self.showLoadingView(false, alpha: 0) - self.notify(event: AdEvent.AdDidRequestResume()) + self.notify(event: AdEvent.AdDidRequestContentResume()) } public func adsManager(_ adsManager: IMAAdsManager!, adDidProgressToTime mediaTime: TimeInterval, totalTime: TimeInterval) { diff --git a/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift b/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift index 21772ee7..e6a0b183 100644 --- a/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift +++ b/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift @@ -85,7 +85,7 @@ public class KalturaLiveStatsPlugin: BaseAnalyticsPlugin { override var playerEventsToRegister: [PlayerEvent.Type] { return [ PlayerEvent.play, - PlayerEvent.playbackParamsUpdated, + PlayerEvent.playbackInfo, PlayerEvent.pause, PlayerEvent.stateChanged ] @@ -111,12 +111,12 @@ public class KalturaLiveStatsPlugin: BaseAnalyticsPlugin { PKLog.debug("pause event: \(event)") strongSelf.stopLiveEvents() } - case let e where e.self == PlayerEvent.playbackParamsUpdated: + case let e where e.self == PlayerEvent.playbackInfo: self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in guard let strongSelf = self else { return } PKLog.debug("playbackParamsUpdated event: \(event)") - if type(of: event) == PlayerEvent.playbackParamsUpdated { - strongSelf.lastReportedBitrate = Int32(event.currentBitrate!) + if type(of: event) == PlayerEvent.playbackInfo && event.playbackInfo != nil { + strongSelf.lastReportedBitrate = Int32(event.playbackInfo!.bitrate) } } case let e where e.self == PlayerEvent.stateChanged: diff --git a/Plugins/Youbora/YouboraAdnalyzerManager.swift b/Plugins/Youbora/YouboraAdnalyzerManager.swift new file mode 100644 index 00000000..b5914c52 --- /dev/null +++ b/Plugins/Youbora/YouboraAdnalyzerManager.swift @@ -0,0 +1,213 @@ +// +// YouboraAdnalyzerManager.swift +// Pods +// +// Created by Gal Orlanczyk on 20/04/2017. +// +// + +import YouboraLib + +class YouboraAdnalyzerManager: YBAdnalyzerGeneric { + + weak var adInfo: PKAdInfo? + var adPlayhead: TimeInterval? + var lastReportedResource: String? + + fileprivate weak var messageBus: MessageBus? + + override init!(pluginInstance plugin: YBPluginGeneric!) { + super.init(pluginInstance: plugin) + self.adnalyzerVersion = YBYouboraLibVersion + "-\(YouboraPlugin.kaltura)-" + PlayKitManager.clientTag // TODO: put plugin version when we will seperate + } + + // we must override this init in order to override the `pluginInstance` init + private override init() { + super.init() + } +} + +/************************************************************/ +// MARK: - Youbora AdnalyzerGeneric +/************************************************************/ + +extension YouboraAdnalyzerManager { + + override func startMonitoring(withPlayer player: NSObject!) { + guard let messageBus = player as? MessageBus else { + assertionFailure("our events handler object must be of type: `MessageBus`") + return + } + super.startMonitoring(withPlayer: nil) // no need to pass our object it is not player type + self.reset() + self.messageBus = messageBus + self.registerAdEvents(onMessageBus: messageBus) + } + + override func stopMonitoring() { + if let messageBus = self.messageBus { + self.unregisterAdEvents(fromMessageBus: messageBus) + } + super.stopMonitoring() + } +} + +/************************************************************/ +// MARK: - Youbora Info Methods +/************************************************************/ + +extension YouboraAdnalyzerManager { + + override func getAdPlayhead() -> NSNumber! { + if let adPlayhead = self.adPlayhead, adPlayhead > 0 { + return NSNumber(value: adPlayhead) + } else { + return super.getAdPlayhead() + } + } + + override func getAdPosition() -> String! { + if let adInfo = self.adInfo { + switch adInfo.positionType { + case .preRoll: return "pre" + case .midRoll: return "mid" + case .postRoll: return "post" + } + } else { + return super.getAdPosition() + } + } + + override func getAdTitle() -> String! { + return adInfo?.title ?? super.getAdTitle() + } + + override func getAdDuration() -> NSNumber! { + if let adInfo = self.adInfo { + return NSNumber(value: adInfo.duration) + } else { + return super.getAdDuration() + } + } + + override func getAdResource() -> String! { + return lastReportedResource ?? super.getAdResource() + } + + override func getAdPlayerVersion() -> String! { + return PlayKitManager.clientTag + } +} + +/************************************************************/ +// MARK: - Events Handling +/************************************************************/ + +extension YouboraAdnalyzerManager { + + private var adEventsToRegister: [AdEvent.Type] { + return [ + AdEvent.adLoaded, + AdEvent.adStarted, + AdEvent.adComplete, + AdEvent.adResumed, + AdEvent.adPaused, + AdEvent.adDidProgressToTime, + AdEvent.adSkipped, + AdEvent.adStartedBuffering, + AdEvent.adPlaybackReady, + AdEvent.adsRequested, + AdEvent.adDidRequestContentResume + ] + } + + fileprivate func registerAdEvents(onMessageBus messageBus: MessageBus) { + PKLog.debug("register ad events") + + self.adEventsToRegister.forEach { event in + PKLog.debug("\(String(describing: type(of: self))) will register event: \(event.self)") + + switch event { + case let e where e.self == AdEvent.adLoaded: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + // update ad info with the new loaded event + self?.adInfo = event.adInfo + // if ad is preroll make sure to call /start event before /adStart + if let positionType = event.adInfo?.positionType, positionType == .preRoll { + self?.plugin.playHandler() + } + self?.playAdHandler() + } + case let e where e.self == AdEvent.adStarted: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + self?.joinAdHandler() + } + case let e where e.self == AdEvent.adComplete: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + self?.endedAdHandler() + self?.adInfo = nil + } + case let e where e.self == AdEvent.adResumed: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + self?.resumeAdHandler() + // if we were coming from background and ad was resumed + // has no effect when already playing ad and resumed because ad was already started. + self?.plugin?.playHandler() + self?.playAdHandler() + self?.joinAdHandler() + } + case let e where e.self == AdEvent.adPaused: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + self?.pauseAdHandler() + } + case let e where e.self == AdEvent.adDidProgressToTime: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + // update ad playhead with new data + self?.adPlayhead = event.adMediaTime?.doubleValue + } + case let e where e.self == AdEvent.adSkipped: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + self?.skipAdHandler() + } + case let e where e.self == AdEvent.adStartedBuffering: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + self?.bufferingAdHandler() + } + case let e where e.self == AdEvent.adPlaybackReady: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + self?.bufferedAdHandler() + } + case let e where e.self == AdEvent.adsRequested: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + self?.lastReportedResource = event.adTagUrl + } + // when ad request the content to resume (finished or error) + // make sure to send /adStop event and clear the info. + case let e where e.self == AdEvent.adDidRequestContentResume: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + self?.endedAdHandler() + self?.adInfo = nil + } + default: assertionFailure("all events must be handled") + } + } + } + + func unregisterAdEvents(fromMessageBus messageBus: MessageBus) { + messageBus.removeObserver(self, events: adEventsToRegister) + } +} + +/************************************************************/ +// MARK: - Internal +/************************************************************/ + +extension YouboraAdnalyzerManager { + + /// resets the plugin's state. + func reset() { + self.adInfo = nil + self.adPlayhead = -1 + self.lastReportedResource = nil + } +} diff --git a/Plugins/Youbora/YouboraManager.swift b/Plugins/Youbora/YouboraManager.swift index 256cf807..1d9a6b66 100644 --- a/Plugins/Youbora/YouboraManager.swift +++ b/Plugins/Youbora/YouboraManager.swift @@ -7,57 +7,279 @@ // import YouboraLib -import YouboraPluginAVPlayer -import Foundation -import UIKit -import AVFoundation -import AVKit class YouboraManager: YBPluginGeneric { - private weak var pkPlayer: Player? - weak var mediaEntry: MediaEntry? - public var currentBitrate: Double? + fileprivate weak var pkPlayer: Player? + var lastReportedResource: String? + /// The last reported playback info. + var playbackInfo: PKPlaybackInfo? - // for some reason we must implement the initializer this way because the way youbora implemented the init. - // this means player and media entry are defined as optionals but they must have values when initialized. - // All the checks for optionals in this class are just because we defined them as optionals but they are not so the checks are irrelevant. - init!(options: NSObject!, player: Player, mediaEntry: MediaEntry) { + fileprivate weak var messageBus: MessageBus? + + /// indicates whether we played for the first time or not. + fileprivate var isFirstPlay: Bool = true + + /// Indicates if we have to delay the endedHandler() (for example when we have post-roll). + fileprivate var shouldDelayEndedHandler = false + + init(options: NSObject!, player: Player) { super.init(options: options) + self.pluginVersion = YBYouboraLibVersion + "-\(YouboraPlugin.kaltura)-" + PlayKitManager.clientTag // TODO: put plugin version when we will seperate self.pkPlayer = player - self.mediaEntry = mediaEntry } + // we must override this init in order to add our init (happens because of interopatability of youbora objc framework with swift). private override init() { super.init() } +} + +/************************************************************/ +// MARK: - Youbora PluginGeneric +/************************************************************/ + +extension YouboraManager { + + override func startMonitoring(withPlayer player: NSObject!) { + guard let messageBus = player as? MessageBus else { + assertionFailure("our events handler object must be of type: `MessageBus`") + return + } + super.startMonitoring(withPlayer: nil) // no need to pass our object it is not player type + self.reset() + self.messageBus = messageBus + self.registerEvents(onMessageBus: messageBus) + } + + override func stopMonitoring() { + if let messageBus = self.messageBus { + self.unregisterEvents(fromMessageBus: messageBus) + } + super.stopMonitoring() + } +} + +/************************************************************/ +// MARK: - Youbora Info Methods +/************************************************************/ + +extension YouboraManager { + + override func getMediaDuration() -> NSNumber! { + let duration = self.pkPlayer?.duration + return duration != nil ? NSNumber(value: duration!) : super.getMediaDuration() + } + + override func getResource() -> String! { + // TODO: make sure to expose player content url and use it here instead of id + // will be added when we will have the relevant data, currently only a placeholder + return self.lastReportedResource ?? super.getResource() + } - /************************************************************/ - // MARK: - Overrides - /************************************************************/ + override func getTitle() -> String! { + return self.pkPlayer?.mediaEntry?.id ?? super.getTitle() + } - override func getMediaDuration() -> NSNumber { - return NSNumber(value: pkPlayer?.duration ?? 0) + override func getPlayhead() -> NSNumber! { + let currentTime = self.pkPlayer?.currentTime + return currentTime != nil ? NSNumber(value: currentTime!) : super.getPlayhead() } - override func getResource() -> String { - PKLog.debug("Resource") - return self.mediaEntry?.id ?? "" + override func getPlayerVersion() -> String! { + return "\(PlayKitManager.clientTag)" } - override func getPlayhead() -> NSNumber { - let currentTIme = self.pkPlayer?.currentTime ?? 0 - return NSNumber(value: currentTIme) + override func getIsLive() -> NSValue! { + if let mediaType = self.pkPlayer?.mediaEntry?.mediaType { + if mediaType == .live { + return NSNumber(value: true) + } + return NSNumber(value: false) + } + return super.getIsLive() } - override func getPlayerVersion() -> String { - return "PlayKit-\(PlayKitManager.versionString)" + override func getBitrate() -> NSNumber! { + if let playbackInfo = self.playbackInfo, playbackInfo.bitrate > 0 { + return NSNumber(value: playbackInfo.bitrate) + } + return super.getBitrate() } - override func getBitrate() -> NSNumber { - if let bitrate = currentBitrate { - return NSNumber(value: bitrate) + override func getThroughput() -> NSNumber! { + if let playbackInfo = self.playbackInfo, playbackInfo.observedBitrate > 0 { + return NSNumber(value: playbackInfo.observedBitrate) } - return NSNumber(value: 0.0) + return super.getThroughput() + } + + override func getRendition() -> String! { + if let pi = self.playbackInfo, pi.indicatedBitrate > 0 && pi.bitrate > 0 && pi.bitrate != pi.indicatedBitrate { + return YBUtils.buildRenditionString(withBitrate: pi.indicatedBitrate) + } + return super.getRendition() + } +} + +/************************************************************/ +// MARK: - Events Handling +/************************************************************/ + +extension YouboraManager { + + private var eventsToRegister: [PKEvent.Type] { + return [ + PlayerEvent.play, + PlayerEvent.stopped, + PlayerEvent.pause, + PlayerEvent.playing, + PlayerEvent.seeking, + PlayerEvent.seeked, + PlayerEvent.ended, + PlayerEvent.playbackInfo, + PlayerEvent.stateChanged, + PlayerEvent.error, + AdEvent.adCuePointsUpdate, + AdEvent.allAdsCompleted + ] + } + + fileprivate func registerEvents(onMessageBus messageBus: MessageBus) { + PKLog.debug("register events") + + self.eventsToRegister.forEach { event in + PKLog.debug("Register event: \(event.self)") + + switch event { + case let e where e.self == PlayerEvent.play: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + guard let strongSelf = self else { return } + // play handler to start when asset starts loading. + // this point is the closest point to prepare call. + strongSelf.playHandler() + strongSelf.postEventLogWithMessage(message: "\(type(of: event))") + } + 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 } + // we must call `endedHandler()` when stopped so youbora will know player stopped playing content. + strongSelf.adnalyzer?.endedAdHandler() + strongSelf.endedHandler() + strongSelf.postEventLogWithMessage(message: "\(type(of: event))") + } + 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 } + strongSelf.pauseHandler() + strongSelf.postEventLogWithMessage(message: "\(type(of: event))") + } + 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 } + if strongSelf.isFirstPlay { + strongSelf.isFirstPlay = false + strongSelf.joinHandler() + strongSelf.bufferedHandler() + } else { + strongSelf.resumeHandler() + } + strongSelf.postEventLogWithMessage(message: "\(String(describing: type(of: event)))") + } + case let e where e.self == PlayerEvent.seeking: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + guard let strongSelf = self else { return } + strongSelf.seekingHandler() + strongSelf.postEventLogWithMessage(message: "\(type(of: event))") + } + case let e where e.self == PlayerEvent.seeked: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + guard let strongSelf = self else { return } + strongSelf.seekedHandler() + strongSelf.postEventLogWithMessage(message: "\(type(of: event))") + } + case let e where e.self == PlayerEvent.ended: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + guard let strongSelf = self else { return } + if !strongSelf.shouldDelayEndedHandler { + strongSelf.endedHandler() + } + strongSelf.postEventLogWithMessage(message: "\(type(of: event))") + } + case let e where e.self == PlayerEvent.playbackInfo: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + guard let strongSelf = self else { return } + strongSelf.playbackInfo = event.playbackInfo + strongSelf.postEventLogWithMessage(message: "\(type(of: event))") + } + case let e where e.self == PlayerEvent.stateChanged: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + guard let strongSelf = self else { return } + if event.newState == .buffering { + strongSelf.bufferingHandler() + strongSelf.postEventLogWithMessage(message: "\(type(of: event))") + } else if event.oldState == .buffering { + strongSelf.bufferedHandler() + strongSelf.postEventLogWithMessage(message: "\(type(of: event))") + } + } + case let e where e.self == PlayerEvent.error: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + guard let strongSelf = self else { return } + if let error = event.error, error.code == PKErrorCode.playerItemFailed { + strongSelf.errorHandler(withCode: "\(error.code)", message: error.localizedDescription, andErrorMetadata: error.description) + } + } + case let e where e.self == AdEvent.adCuePointsUpdate: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + if let hasPostRoll = event.adCuePoints?.hasPostRoll, hasPostRoll == true { + self?.shouldDelayEndedHandler = true + } + } + case let e where e.self == AdEvent.allAdsCompleted: + messageBus.addObserver(self, events: [e.self]) { [weak self] event in + if let shouldDelayEndedHandler = self?.shouldDelayEndedHandler, shouldDelayEndedHandler == true { + self?.shouldDelayEndedHandler = false + self?.adnalyzer?.endedAdHandler() + } + } + default: assertionFailure("all events must be handled") + } + } + } + + fileprivate func unregisterEvents(fromMessageBus messageBus: MessageBus) { + messageBus.removeObserver(self, events: eventsToRegister) + } +} + +/************************************************************/ +// MARK: - Internal +/************************************************************/ + +extension YouboraManager { + + func resetForBackground() { + self.playbackInfo = nil + self.isFirstPlay = true + } + + func reset() { + self.playbackInfo = nil + self.lastReportedResource = nil + self.isFirstPlay = true + self.shouldDelayEndedHandler = false + } +} + +/************************************************************/ +// MARK: - Private +/************************************************************/ + +extension YouboraManager { + + fileprivate func postEventLogWithMessage(message: String) { + let eventLog = YouboraEvent.Report(message: message) + self.messageBus?.post(eventLog) } } diff --git a/Plugins/Youbora/YouboraPlugin.swift b/Plugins/Youbora/YouboraPlugin.swift index 03d50c1e..0ba586a0 100644 --- a/Plugins/Youbora/YouboraPlugin.swift +++ b/Plugins/Youbora/YouboraPlugin.swift @@ -7,268 +7,186 @@ // import YouboraLib -import YouboraPluginAVPlayer -import AVFoundation /************************************************************/ -// MARK: - YouboraPluginError +// MARK: - YouboraPlugin /************************************************************/ -/// `YouboraPluginError` represents youbora plugin errors. -enum YouboraPluginError: PKError { - - case failedToSetupYouboraManager - - static let domain = "com.kaltura.playkit.error.youbora" +public class YouboraPlugin: BasePlugin, AppStateObservable { - var code: Int { - switch self { - case .failedToSetupYouboraManager: return PKErrorCode.failedToSetupYouboraManager - } + struct CustomPropertyKey { + static let sessionId = "sessionId" } - var errorDescription: String { - switch self { - case .failedToSetupYouboraManager: return "failed to setup youbora manager, missing config/config params or mediaEntry" - } - } - - var userInfo: [String: Any] { - switch self { - case .failedToSetupYouboraManager: return [:] - } - } -} - -extension PKErrorDomain { - @objc(Youbora) public static let youbora = YouboraPluginError.domain -} - -extension PKErrorCode { - @objc(FailedToSetupYouboraManager) public static let failedToSetupYouboraManager = 2200 -} - -/************************************************************/ -// MARK: - YouboraPlugin -/************************************************************/ - -public class YouboraPlugin: BaseAnalyticsPlugin { - public override class var pluginName: String { return "YouboraPlugin" } - private var youboraManager : YouboraManager? + /// The key for enabling adnalyzer in the config dictionary + public static let enableSmartAdsKey = "enableSmartAds" + + public static let kaltura = "kaltura" + + /// The youbora plugin inheriting from `YBPluginGeneric` + /// - important: Make sure to call `playHandler()` at the start of any flow before everying + /// (for example before pre-roll in ads) also make sure to call `endedHandler() at the end of every flow + /// (for example when we have post-roll call it after the ad). + /// In addition, when content ends in the middle also make sure to call `endedHandler()` + /// otherwise youbora will wait for /stop event and you could not start new content events until /stop is received. + private var youboraManager: YouboraManager + private var adnalyzerManager: YouboraAdnalyzerManager? + + /// The plugin's config + var config: AnalyticsConfig /************************************************************/ // MARK: - PKPlugin /************************************************************/ public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) throws { - try super.init(player: player, pluginConfig: pluginConfig, messageBus: messageBus) - guard let _ = pluginConfig as? AnalyticsConfig else { + guard let config = pluginConfig as? AnalyticsConfig else { PKLog.error("missing plugin config") throw PKPluginError.missingPluginConfig(pluginName: YouboraPlugin.pluginName) } + self.config = config + /// initialize youbora components + let options = config.params + let optionsObject = NSDictionary(dictionary: options) + self.youboraManager = YouboraManager(options: optionsObject, player: player) + if let enableSmartAds = config.params[YouboraPlugin.enableSmartAdsKey] as? Bool, enableSmartAds == true { + self.adnalyzerManager = YouboraAdnalyzerManager(pluginInstance: self.youboraManager) + self.youboraManager.adnalyzer = self.adnalyzerManager + } + + try super.init(player: player, pluginConfig: pluginConfig, messageBus: messageBus) + + // start monitoring for events + self.startMonitoring() + // monitor app state changes + AppStateSubject.shared.add(observer: self) + + self.setupYoubora(withConfig: config) } public override func onUpdateMedia(mediaConfig: MediaConfig) { super.onUpdateMedia(mediaConfig: mediaConfig) - self.setupYouboraManager() { succeeded in - if let player = self.player, succeeded { - self.startMonitoring(player: player) - } - } + // in case we stopped playback in the middle call eneded handlers and reset state. + self.endedHandler() + self.adnalyzerManager?.reset() + self.youboraManager.reset() + self.setupYoubora(withConfig: self.config) } public override func onUpdateConfig(pluginConfig: Any) { super.onUpdateConfig(pluginConfig: pluginConfig) - self.setupYouboraManager() + guard let config = pluginConfig as? AnalyticsConfig else { + PKLog.error("wrong config, could not setup youbora manager") + self.messageBus?.post(PlayerEvent.PluginError(nsError: YouboraPluginError.failedToSetupYouboraManager.asNSError)) + return + } + self.config = config + self.setupYoubora(withConfig: config) + // make sure to create or destroy adnalyzer based on config + if let enableSmartAds = config.params[YouboraPlugin.enableSmartAdsKey] as? Bool { + if enableSmartAds == true && self.adnalyzerManager == nil { + self.adnalyzerManager = YouboraAdnalyzerManager(pluginInstance: self.youboraManager) + self.youboraManager.adnalyzer = self.adnalyzerManager + self.startMonitoringAdnalyzer() + } else if enableSmartAds == false && self.adnalyzerManager != nil { + self.stopMonitoringAdnalyzer() + self.adnalyzerManager = nil + } + } } public override func destroy() { - super.destroy() + // we must call `endedHandler()` when destroyed so youbora will know player stopped playing content. + self.endedHandler() self.stopMonitoring() + // remove ad observers + self.messageBus?.removeObserver(self, events: [AdEvent.adCuePointsUpdate, AdEvent.allAdsCompleted]) + AppStateSubject.shared.remove(observer: self) + super.destroy() } /************************************************************/ - // MARK: - AnalytisPluginProtocol + // MARK: - App State Handling /************************************************************/ - override var playerEventsToRegister: [PlayerEvent.Type] { + var observations: Set { return [ - PlayerEvent.canPlay, - PlayerEvent.play, - PlayerEvent.pause, - PlayerEvent.playing, - PlayerEvent.seeking, - PlayerEvent.seeked, - PlayerEvent.ended, - PlayerEvent.playbackParamsUpdated, - PlayerEvent.stateChanged - ] - } - - override func registerEvents() { - PKLog.debug("register player events") - - self.playerEventsToRegister.forEach { event in - PKLog.debug("Register event: \(event.self)") - - switch event { - case let e where e.self == PlayerEvent.canPlay: - self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in - guard let strongSelf = self else { return } - strongSelf.postEventLogWithMessage(message: "canPlay event: \(event)") - } - case let e where e.self == PlayerEvent.play: - self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in - guard let strongSelf = self else { return } - guard let youboraManager = strongSelf.youboraManager else { return } - youboraManager.playHandler() - strongSelf.postEventLogWithMessage(message: "play event: \(event)") - } - 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 } - guard let youboraManager = strongSelf.youboraManager else { return } - youboraManager.pauseHandler() - strongSelf.postEventLogWithMessage(message: "pause event: \(event)") - } - 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 } - strongSelf.postEventLogWithMessage(message: "playing event: \(event)") - - guard let youboraManager = strongSelf.youboraManager else { return } - - if strongSelf.isFirstPlay { - youboraManager.joinHandler() - youboraManager.bufferedHandler() - strongSelf.isFirstPlay = false - } else { - youboraManager.resumeHandler() - } - } - case let e where e.self == PlayerEvent.seeking: - self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in - guard let strongSelf = self else { return } - guard let youboraManager = strongSelf.youboraManager else { return } - youboraManager.seekingHandler() - strongSelf.postEventLogWithMessage(message: "seeking event: \(event)") - } - case let e where e.self == PlayerEvent.seeked: - self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in - guard let strongSelf = self else { return } - guard let youboraManager = strongSelf.youboraManager else { return } - youboraManager.seekedHandler() - strongSelf.postEventLogWithMessage(message: "seeked event: \(event)") - } - 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 } - guard let youboraManager = strongSelf.youboraManager else { return } - youboraManager.endedHandler() - strongSelf.postEventLogWithMessage(message: "ended event: \(event)") - } - case let e where e.self == PlayerEvent.playbackParamsUpdated: - self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in - guard let strongSelf = self else { return } - guard let youboraManager = strongSelf.youboraManager else { return } - youboraManager.currentBitrate = event.currentBitrate?.doubleValue - strongSelf.postEventLogWithMessage(message: "playbackParamsUpdated event: \(event)") - } - case let e where e.self == PlayerEvent.stateChanged: - self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in - guard let strongSelf = self else { return } - guard let youboraManager = strongSelf.youboraManager else { return } - switch event.newState { - case .buffering: - youboraManager.bufferingHandler() - strongSelf.postEventLogWithMessage(message: "Buffering event: ֿ\(event)") - break - default: break - } - - switch event.oldState { - case .buffering: - youboraManager.bufferedHandler() - strongSelf.postEventLogWithMessage(message: "Buffered event: \(event)") - break - default: break - } - } - default: assertionFailure("all events must be handled") + NotificationObservation(name: .UIApplicationWillTerminate) { [unowned self] in + PKLog.debug("youbora plugin will terminate event received") + // we must call `endedHandler()` when stopped so youbora will know player stopped playing content. + self.endedHandler() + AppStateSubject.shared.remove(observer: self) + }, + NotificationObservation(name: .UIApplicationDidEnterBackground) { [unowned self] in + // when entering background we should call `endedHandler()` to make sure coming back starts a new session. + // otherwise events could be lost (youbora only retry sending events for 5 minutes). + self.endedHandler() + // reset the youbora plugin for background handling to start playing again when we return. + self.youboraManager.resetForBackground() } - } - - PKLog.debug("register ads events") - self.messageBus?.addObserver(self, events: AdEvent.allEventTypes) { [weak self] event in - self?.postEventLogWithMessage(message: "Ads event event: \(event)") - } + ] } /************************************************************/ // MARK: - Private /************************************************************/ - private func setupYouboraManager(completionHandler: ((_ succeeded: Bool) -> Void)? = nil) { - guard let player = self.player else { return } - guard let mediaEntry = player.mediaEntry else { - PKLog.error("missing MediaEntry, could not setup youbora manager") - self.messageBus?.post(PlayerEvent.PluginError(nsError: YouboraPluginError.failedToSetupYouboraManager.asNSError)) - completionHandler?(false) - return - } - - guard let config = self.config else { - PKLog.error("config params doesn't exist, could not setup youbora manager") - self.messageBus?.post(PlayerEvent.PluginError(nsError: YouboraPluginError.failedToSetupYouboraManager.asNSError)) - completionHandler?(false) - return - } - - var options = [String: Any]() - - // if media exists overwrite using the new info, else create a new media dictionary - if var media = config.params["media"] as? [String: Any] { - media["resource"] = mediaEntry.id - media["title"] = mediaEntry.id - media["duration"] = player.duration - config.params["media"] = media - } else { - config.params["media"] = [ - "resource" : mediaEntry.id, - "title" : mediaEntry.id, - "duration" : mediaEntry.duration - ] - } - options = config.params - - // if youbora manager already created just update options - if let youboraManager = self.youboraManager { - youboraManager.setOptions(options as NSObject!) - } else { - self.youboraManager = YouboraManager(options: options as NSObject!, player: player, mediaEntry: mediaEntry) - } - completionHandler?(true) + private func setupYoubora(withConfig config: AnalyticsConfig) { + var options = config.params + self.addCustomProperties(toOptions: &options) + let optionsObject = NSDictionary(dictionary: options) + self.youboraManager.setOptions(optionsObject) } - private func startMonitoring(player: Player) { - guard let youboraManager = self.youboraManager else { return } - PKLog.debug("Start monitoring using Youbora") - youboraManager.startMonitoring(withPlayer: youboraManager) + private func startMonitoring() { + // make sure to first stop monitoring in case we of uneven call to start/stop + self.stopMonitoring() + PKLog.debug("Start monitoring Youbora") + self.youboraManager.startMonitoring(withPlayer: self.messageBus) + self.startMonitoringAdnalyzer() } private func stopMonitoring() { - guard let youboraManager = self.youboraManager else { return } + self.stopMonitoringAdnalyzer() PKLog.debug("Stop monitoring using Youbora") - youboraManager.stopMonitoring() + self.youboraManager.stopMonitoring() + } + + private func endedHandler() { + self.adnalyzerManager?.endedAdHandler() + self.youboraManager.endedHandler() } - private func postEventLogWithMessage(message: String) { - PKLog.debug(message) - let eventLog = YouboraEvent.Report(message: message) - self.messageBus?.post(eventLog) + private func startMonitoringAdnalyzer() { + if let adnalyerManager = self.adnalyzerManager { + PKLog.debug("Start monitoring Youbora Adnalyzer") + // we start monitoring using messageBus object because he is the one handling our events not the player + adnalyerManager.startMonitoring(withPlayer: self.messageBus) + } + } + + private func stopMonitoringAdnalyzer() { + if let adnalyerManager = self.adnalyzerManager { + PKLog.debug("Stop monitoring using Youbora Adnalyzer") + adnalyerManager.stopMonitoring() + } + } + + private func addCustomProperties(toOptions options: inout [String: Any]) { + guard let player = self.player else { + PKLog.warning("couldn't add custom properties, player instance is nil") + return + } + let propertiesKey = "properties" + if var properties = options[propertiesKey] as? [String: Any] { // if properties already exists override the custom properties only + properties[CustomPropertyKey.sessionId] = player.sessionId + options[propertiesKey] = properties + } else { // if properties doesn't exist then add + options[propertiesKey] = [CustomPropertyKey.sessionId: player.sessionId] + } } } diff --git a/Plugins/Youbora/YouboraPluginError.swift b/Plugins/Youbora/YouboraPluginError.swift new file mode 100644 index 00000000..172d3e5a --- /dev/null +++ b/Plugins/Youbora/YouboraPluginError.swift @@ -0,0 +1,43 @@ +// +// YouboraPluginError.swift +// Pods +// +// Created by Gal Orlanczyk on 24/04/2017. +// +// + +import Foundation + +/// `YouboraPluginError` represents youbora plugin errors. +enum YouboraPluginError: PKError { + + case failedToSetupYouboraManager + + static let domain = "com.kaltura.playkit.error.youbora" + + var code: Int { + switch self { + case .failedToSetupYouboraManager: return PKErrorCode.failedToSetupYouboraManager + } + } + + var errorDescription: String { + switch self { + case .failedToSetupYouboraManager: return "failed to setup youbora manager, missing config/config params or mediaEntry" + } + } + + var userInfo: [String: Any] { + switch self { + case .failedToSetupYouboraManager: return [:] + } + } +} + +extension PKErrorDomain { + @objc(Youbora) public static let youbora = YouboraPluginError.domain +} + +extension PKErrorCode { + @objc(FailedToSetupYouboraManager) public static let failedToSetupYouboraManager = 2200 +}