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/Providers/Base/FormatsHelper.swift b/Classes/Backend/Base/FormatsHelper.swift similarity index 100% rename from Classes/Providers/Base/FormatsHelper.swift rename to Classes/Backend/Base/FormatsHelper.swift diff --git a/Classes/Network/RequestExtension.swift b/Classes/Backend/Base/RequestExtension.swift similarity index 91% rename from Classes/Network/RequestExtension.swift rename to Classes/Backend/Base/RequestExtension.swift index 13c3e471..fabd88c8 100644 --- a/Classes/Network/RequestExtension.swift +++ b/Classes/Backend/Base/RequestExtension.swift @@ -8,6 +8,7 @@ import UIKit import SwiftyJSON +import KalturaNetKit extension KalturaRequestBuilder { @@ -25,8 +26,9 @@ extension KalturaRequestBuilder { } @discardableResult - internal func setFormat(format: Int){ + internal func setFormat(format: Int) -> Self { self.setBody(key: "format", value: JSON(format)) + return self } diff --git a/Classes/Providers/OTT/Model/OTTBaseObject.swift b/Classes/Backend/OTT/Model/OTTBaseObject.swift similarity index 100% rename from Classes/Providers/OTT/Model/OTTBaseObject.swift rename to Classes/Backend/OTT/Model/OTTBaseObject.swift diff --git a/Classes/Providers/OTT/Model/OTTDrmData.swift b/Classes/Backend/OTT/Model/OTTDrmData.swift similarity index 100% rename from Classes/Providers/OTT/Model/OTTDrmData.swift rename to Classes/Backend/OTT/Model/OTTDrmData.swift diff --git a/Classes/Providers/OTT/Model/OTTError.swift b/Classes/Backend/OTT/Model/OTTError.swift similarity index 100% rename from Classes/Providers/OTT/Model/OTTError.swift rename to Classes/Backend/OTT/Model/OTTError.swift diff --git a/Classes/Providers/OTT/Model/OTTPlaybackContext.swift b/Classes/Backend/OTT/Model/OTTPlaybackContext.swift similarity index 100% rename from Classes/Providers/OTT/Model/OTTPlaybackContext.swift rename to Classes/Backend/OTT/Model/OTTPlaybackContext.swift diff --git a/Classes/Providers/OTT/Model/OTTPlaybackSource.swift b/Classes/Backend/OTT/Model/OTTPlaybackSource.swift similarity index 100% rename from Classes/Providers/OTT/Model/OTTPlaybackSource.swift rename to Classes/Backend/OTT/Model/OTTPlaybackSource.swift diff --git a/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift b/Classes/Backend/OTT/Parsers/OTTMultiResponseParser.swift similarity index 92% rename from Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift rename to Classes/Backend/OTT/Parsers/OTTMultiResponseParser.swift index 88822a76..0b6b0d51 100644 --- a/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift +++ b/Classes/Backend/OTT/Parsers/OTTMultiResponseParser.swift @@ -28,8 +28,6 @@ class OTTMultiResponseParser: NSObject { let objectType: OTTBaseObject.Type? = OTTObjectMapper.classByJsonObject(json: jsonObject.dictionaryObject) if let type = objectType { object = type.init(json: jsonObject.object) - } else { - throw OTTMultiResponseParserError.typeNotFound } if let obj = object { diff --git a/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift b/Classes/Backend/OTT/Parsers/OTTObjectMapper.swift similarity index 74% rename from Classes/Providers/OTT/Parsers/OTTObjectMapper.swift rename to Classes/Backend/OTT/Parsers/OTTObjectMapper.swift index eb5250ab..1c6ebcdc 100644 --- a/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift +++ b/Classes/Backend/OTT/Parsers/OTTObjectMapper.swift @@ -21,14 +21,6 @@ class OTTObjectMapper: NSObject { if let name = className { switch name { - case "KalturaLoginResponse": - return OTTLoginResponse.self - case "KalturaSession": - return OTTSession.self - case "KalturaMediaAsset": - return OTTAsset.self - case "KalturaLoginSession": - return OTTLoginSession.self case "KalturaPlaybackSource": return OTTPlaybackSource.self case "KalturaPlaybackContext": diff --git a/Classes/Providers/OTT/Parsers/OTTResponseParser.swift b/Classes/Backend/OTT/Parsers/OTTResponseParser.swift similarity index 95% rename from Classes/Providers/OTT/Parsers/OTTResponseParser.swift rename to Classes/Backend/OTT/Parsers/OTTResponseParser.swift index f771ee7d..8de83acc 100644 --- a/Classes/Providers/OTT/Parsers/OTTResponseParser.swift +++ b/Classes/Backend/OTT/Parsers/OTTResponseParser.swift @@ -9,7 +9,7 @@ import UIKit import SwiftyJSON -class OTTResponseParser: ResponseParser { +class OTTResponseParser: NSObject { enum OTTResponseParserError: Error { case typeNotFound diff --git a/Classes/Providers/OTT/Services/OTTAssetService.swift b/Classes/Backend/OTT/Services/OTTAssetService.swift similarity index 56% rename from Classes/Providers/OTT/Services/OTTAssetService.swift rename to Classes/Backend/OTT/Services/OTTAssetService.swift index 476d19c5..0200e12a 100644 --- a/Classes/Providers/OTT/Services/OTTAssetService.swift +++ b/Classes/Backend/OTT/Services/OTTAssetService.swift @@ -8,25 +8,11 @@ import UIKit import SwiftyJSON +import KalturaNetKit class OTTAssetService { - internal static func get(baseURL: String, ks: String, assetId: String, type: AssetType) -> KalturaRequestBuilder? { - - if let request: KalturaRequestBuilder = KalturaRequestBuilder(url: baseURL, service: "asset", action: "get") { - request - .setBody(key: "id", value: JSON(assetId)) - .setBody(key: "ks", value: JSON(ks)) - .setBody(key: "type", value: JSON(type.asString)) - .setBody(key: "assetReferenceType", value: JSON(type.asString)) - .setBody(key: "with", value: JSON([["type": "files", "objectType": "KalturaCatalogWithHolder"]])) - return request - } else { - return nil - } - } - - internal static func getPlaybackContext(baseURL: String, ks: String, assetId: String, type: AssetType, playbackContextOptions: PlaybackContextOptions) -> KalturaRequestBuilder? { + internal static func getPlaybackContext(baseURL: String, ks: String, assetId: String, type: AssetObjectType, playbackContextOptions: PlaybackContextOptions) -> KalturaRequestBuilder? { if let request: KalturaRequestBuilder = KalturaRequestBuilder(url: baseURL, service: "asset", action: "getPlaybackContext") { request @@ -38,13 +24,12 @@ class OTTAssetService { } else { return nil } - } } struct PlaybackContextOptions { - internal var playbackContextType: PlaybackContextType + internal var playbackContextType: PlaybackType internal var protocls: [String] internal var assetFileIds: [String]? diff --git a/Classes/Backend/OTT/Services/OTTUserService.swift b/Classes/Backend/OTT/Services/OTTUserService.swift new file mode 100644 index 00000000..95e251d9 --- /dev/null +++ b/Classes/Backend/OTT/Services/OTTUserService.swift @@ -0,0 +1,27 @@ +// +// OTTUserService.swift +// Pods +// +// Created by Admin on 13/11/2016. +// +// + +import UIKit +import SwiftyJSON +import KalturaNetKit + +class OTTUserService: NSObject { + + internal static func anonymousLogin(baseURL: String, partnerId: Int64, udid: String? = nil) -> KalturaRequestBuilder? { + if let request = KalturaRequestBuilder(url: baseURL, service: "ottUser", action: "anonymousLogin") { + request.setBody(key: "partnerId", value: JSON(NSNumber.init(value: partnerId))) + + if let deviceId = udid { + request.setBody(key: "udid", value: JSON(deviceId)) + } + return request + } + return nil + } + +} diff --git a/Classes/Providers/OTT/Services/PhoenixAPIDefines.swift b/Classes/Backend/OTT/Services/PhoenixAPIDefines.swift similarity index 89% rename from Classes/Providers/OTT/Services/PhoenixAPIDefines.swift rename to Classes/Backend/OTT/Services/PhoenixAPIDefines.swift index 9255c999..5c074b55 100644 --- a/Classes/Providers/OTT/Services/PhoenixAPIDefines.swift +++ b/Classes/Backend/OTT/Services/PhoenixAPIDefines.swift @@ -9,7 +9,7 @@ import Foundation -@objc public enum AssetType: Int { +enum AssetObjectType: Int { case media case epg case unknown @@ -24,7 +24,7 @@ import Foundation } -@objc public enum PlaybackContextType: Int { +enum PlaybackType: Int { case trailer case catchup diff --git a/Classes/Providers/OVP/Model/OVPBaseObject.swift b/Classes/Backend/OVP/Model/OVPBaseObject.swift similarity index 100% rename from Classes/Providers/OVP/Model/OVPBaseObject.swift rename to Classes/Backend/OVP/Model/OVPBaseObject.swift diff --git a/Classes/Providers/OVP/Model/OVPDRM.swift b/Classes/Backend/OVP/Model/OVPDRM.swift similarity index 100% rename from Classes/Providers/OVP/Model/OVPDRM.swift rename to Classes/Backend/OVP/Model/OVPDRM.swift diff --git a/Classes/Providers/OVP/Model/OVPEntry.swift b/Classes/Backend/OVP/Model/OVPEntry.swift similarity index 100% rename from Classes/Providers/OVP/Model/OVPEntry.swift rename to Classes/Backend/OVP/Model/OVPEntry.swift diff --git a/Classes/Providers/OVP/Model/OVPError.swift b/Classes/Backend/OVP/Model/OVPError.swift similarity index 100% rename from Classes/Providers/OVP/Model/OVPError.swift rename to Classes/Backend/OVP/Model/OVPError.swift diff --git a/Classes/Providers/OVP/Model/OVPFlavorAsset.swift b/Classes/Backend/OVP/Model/OVPFlavorAsset.swift similarity index 100% rename from Classes/Providers/OVP/Model/OVPFlavorAsset.swift rename to Classes/Backend/OVP/Model/OVPFlavorAsset.swift diff --git a/Classes/Providers/OVP/Model/OVPList.swift b/Classes/Backend/OVP/Model/OVPList.swift similarity index 100% rename from Classes/Providers/OVP/Model/OVPList.swift rename to Classes/Backend/OVP/Model/OVPList.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/Providers/OVP/Model/OVPMetadata.swift b/Classes/Backend/OVP/Model/OVPMetadata.swift similarity index 100% rename from Classes/Providers/OVP/Model/OVPMetadata.swift rename to Classes/Backend/OVP/Model/OVPMetadata.swift diff --git a/Classes/Providers/OVP/Model/OVPPlaybackContext.swift b/Classes/Backend/OVP/Model/OVPPlaybackContext.swift similarity index 100% rename from Classes/Providers/OVP/Model/OVPPlaybackContext.swift rename to Classes/Backend/OVP/Model/OVPPlaybackContext.swift diff --git a/Classes/Providers/OVP/Model/OVPSource.swift b/Classes/Backend/OVP/Model/OVPSource.swift similarity index 100% rename from Classes/Providers/OVP/Model/OVPSource.swift rename to Classes/Backend/OVP/Model/OVPSource.swift diff --git a/Classes/Providers/OVP/Model/OVPStartWidgetSessionResponse.swift b/Classes/Backend/OVP/Model/OVPStartWidgetSessionResponse.swift similarity index 100% rename from Classes/Providers/OVP/Model/OVPStartWidgetSessionResponse.swift rename to Classes/Backend/OVP/Model/OVPStartWidgetSessionResponse.swift diff --git a/Classes/Providers/OVP/Parsers/OVPMultiResponseParser.swift b/Classes/Backend/OVP/Parsers/OVPMultiResponseParser.swift similarity index 100% rename from Classes/Providers/OVP/Parsers/OVPMultiResponseParser.swift rename to Classes/Backend/OVP/Parsers/OVPMultiResponseParser.swift diff --git a/Classes/Providers/OVP/Parsers/OVPObjectMapper.swift b/Classes/Backend/OVP/Parsers/OVPObjectMapper.swift similarity index 82% rename from Classes/Providers/OVP/Parsers/OVPObjectMapper.swift rename to Classes/Backend/OVP/Parsers/OVPObjectMapper.swift index 953d0b99..0ad39424 100644 --- a/Classes/Providers/OVP/Parsers/OVPObjectMapper.swift +++ b/Classes/Backend/OVP/Parsers/OVPObjectMapper.swift @@ -17,18 +17,22 @@ class OVPObjectMapper: NSObject { static func classByJsonObject(json:Any?) -> OVPBaseObject.Type? { - let jsonObject = JSON(json) + guard let js = json else { + return nil + } + + let jsonObject = JSON(js) let className = jsonObject[classNameKey].string if let name = className{ switch name { case "KalturaMediaEntry": return OVPEntry.self + case "KalturaLiveStreamEntry": + return OVPLiveStreamEntry.self case "KalturaPlaybackContext": return OVPPlaybackContext.self case "KalturaAPIException": return OVPError.self - case "KalturaStartWidgetSessionResponse": - return OVPStartWidgetSessionResponse.self case "KalturaMetadata": return OVPMetadata.self default: diff --git a/Classes/Providers/OVP/Services/OVPBaseEntryService.swift b/Classes/Backend/OVP/Services/OVPBaseEntryService.swift similarity index 99% rename from Classes/Providers/OVP/Services/OVPBaseEntryService.swift rename to Classes/Backend/OVP/Services/OVPBaseEntryService.swift index 300fe279..107acacb 100644 --- a/Classes/Providers/OVP/Services/OVPBaseEntryService.swift +++ b/Classes/Backend/OVP/Services/OVPBaseEntryService.swift @@ -8,6 +8,7 @@ import UIKit import SwiftyJSON +import KalturaNetKit class OVPBaseEntryService { diff --git a/Classes/Providers/OVP/Services/OVPSessionService.swift b/Classes/Backend/OVP/Services/OVPSessionService.swift similarity index 51% rename from Classes/Providers/OVP/Services/OVPSessionService.swift rename to Classes/Backend/OVP/Services/OVPSessionService.swift index 777d6ec5..97c92b84 100644 --- a/Classes/Providers/OVP/Services/OVPSessionService.swift +++ b/Classes/Backend/OVP/Services/OVPSessionService.swift @@ -8,22 +8,10 @@ import UIKit import SwiftyJSON +import KalturaNetKit class OVPSessionService { - - internal static func get(baseURL: String, - ks: String) -> KalturaRequestBuilder? { - if let request: KalturaRequestBuilder = KalturaRequestBuilder(url: baseURL, - service: "session", - action: "get") { - request.setBody(key: "ks", value: JSON(ks)) - return request - }else{ - return nil - } - } - internal static func startWidgetSession(baseURL: String, partnerId: Int64 ) -> KalturaRequestBuilder? { @@ -38,14 +26,4 @@ class OVPSessionService { } } - - - - -// .service("session") -// .action("startWidgetSession") -// .method("POST") -// .url(baseUrl) -// .tag("session-startWidget") -// .params(params); } 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 } } diff --git a/Classes/Network/KalturaMultiRequestBuilder.swift b/Classes/Network/KalturaMultiRequestBuilder.swift deleted file mode 100644 index 57d5fc00..00000000 --- a/Classes/Network/KalturaMultiRequestBuilder.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// RestMultiRequestBuilder.swift -// Pods -// -// Created by Admin on 13/11/2016. -// -// - -import UIKit -import SwiftyJSON - -class KalturaMultiRequestBuilder: KalturaRequestBuilder { - - var requests: [KalturaRequestBuilder] = [KalturaRequestBuilder]() - - init?(url: String) { - super.init(url: url, service: "multirequest", action: nil) - } - - @discardableResult - internal func add(request:KalturaRequestBuilder) -> Self { - self.requests.append(request) - return self - } - - override public func build() -> Request { - - let data = self.kalturaMultiRequestData() - let request = RequestElement(requestId: self.requestId, method: self.method, url: self.url, dataBody: data, headers: self.headers, timeout: self.timeout, configuration: self.configuration, responseSerializer: self.responseSerializer, completion: self.completion) - - return request - } - - func kalturaMultiRequestData() -> Data? { - - if self.jsonBody == nil { - self.jsonBody = JSON([String: Any]()) - } - - for (index, request) in self.requests.enumerated() { - if let body = request.jsonBody { - var singleRequestBody: JSON = body - singleRequestBody["action"] = JSON(request.action ?? "") - singleRequestBody["service"] = JSON(request.service ?? "") - self.jsonBody?[String(index+1)] = singleRequestBody - } - } - - let prefix = "{" - let suffix = "}" - var data = prefix.data(using: String.Encoding.utf8) - - for index in 1...self.requests.count { - let requestBody = self.jsonBody?[String(index)].rawString(String.Encoding.utf8, options: JSONSerialization.WritingOptions())?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - let requestBodyData = requestBody?.data(using: String.Encoding.utf8) - data?.append("\"\(index)\":".data(using: String.Encoding.utf8)!) - data?.append(requestBodyData!) - data?.append(",".data(using: String.Encoding.utf8)!) - _ = self.jsonBody?.dictionaryObject?.removeValue(forKey: String(index)) - } - - if let jsonBody = self.jsonBody{ - let remainingJsonAsString: String? = jsonBody.rawString(String.Encoding.utf8, options: JSONSerialization.WritingOptions()) - if let jsonString = remainingJsonAsString{ - var jsonWithoutLastChar = String(jsonString.characters.dropLast()) - - jsonWithoutLastChar = String(jsonWithoutLastChar.characters.dropFirst()) - data?.append((jsonWithoutLastChar.data(using: String.Encoding.utf8))!) - } - } - - data?.append(suffix.data(using: String.Encoding.utf8)!) - - return data - } -} - - 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/Network/KalturaRequestBuilder.swift b/Classes/Network/KalturaRequestBuilder.swift deleted file mode 100644 index b3925f97..00000000 --- a/Classes/Network/KalturaRequestBuilder.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// RestRequestBuilder.swift -// Pods -// -// Created by Admin on 13/11/2016. -// -// - -import UIKit - - -class KalturaRequestBuilder: RequestBuilder { - - public var service: String? - public var action: String? - - init?(url: String?, service: String?, action: String?) { - - guard let baseURL = url else { - return nil - } - - var path = baseURL - if let service = service { - self.service = service - let serviceSuffix = "/service/" + service - path += serviceSuffix - } - - if let action = action { - self.action = action - let actionSuffix = "/action/" + action - path += actionSuffix - } - - super.init(url: path) - - self.add(headerKey: "Content-Type", headerValue: "application/json").add(headerKey: "Accept", headerValue: "application/json") - self.set(method: .post) - - } - - -} diff --git a/Classes/Network/Parser.swift b/Classes/Network/Parser.swift deleted file mode 100644 index 4213eac6..00000000 --- a/Classes/Network/Parser.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// Parser.swift -// Pods -// -// Created by Admin on 23/11/2016. -// -// - -import UIKit - -public protocol ResponseParser { } diff --git a/Classes/Network/Request.swift b/Classes/Network/Request.swift deleted file mode 100644 index c83f0daf..00000000 --- a/Classes/Network/Request.swift +++ /dev/null @@ -1,190 +0,0 @@ -// -// Request.swift -// Pods -// -// Created by Admin on 10/11/2016. -// -// - -import UIKit -import SwiftyJSON - -public typealias completionClosures = (_ response: Response) -> Void - -public enum RequestMethod { - case get - case post - - /// The `RequestMethod` value, for example for get we need "GET" etc. - var value: String { - switch self { - case .get: return "GET" - case .post: return "POST" - } - } -} - -public protocol Request { - - var requestId: String { get } - var method: RequestMethod? { get } - var url: URL { get } - var dataBody: Data? { get } - var headers: [String:String]? { get } - var timeout: Double { get } - var configuration: RequestConfiguration? { get } - var completion: completionClosures? { get } - var responseSerializer: ResponseSerializer { get } -} - -public struct RequestElement: Request { - - public var requestId: String - public var method: RequestMethod? - public var url: URL - public var dataBody: Data? - public var headers: [String:String]? - public var timeout: Double - public var configuration: RequestConfiguration? - public var responseSerializer: ResponseSerializer - public var completion: completionClosures? -} - -@objc public class RequestBuilder: NSObject { - - public lazy var requestId: String = { - return UUID().uuidString - }() - - public var method: RequestMethod? = nil - public var url: URL - public var jsonBody: JSON? = nil - public var headers: [String:String]? = nil - public var timeout: Double = 3 - public var configuration: RequestConfiguration? = nil - public var completion: completionClosures? = nil - public var urlParams: [String: String]? = nil - public var responseSerializer : ResponseSerializer = JSONSerializer() - public init?(url: String){ - if let path = URL(string: url) { - self.url = path - } else { - return nil - } - } - - @discardableResult - public func set(url: URL) -> Self{ - self.url = url - return self - } - - @discardableResult - public func set(method: RequestMethod?) -> Self{ - self.method = method - return self - } - - @discardableResult - public func set(jsonBody:JSON?) -> Self{ - self.jsonBody = jsonBody - return self - } - - @discardableResult - public func set(headers: [String: String]?) -> Self{ - self.headers = headers - return self - } - - @discardableResult - public func set(configuration:RequestConfiguration?) -> Self{ - self.configuration = configuration - return self - } - - @discardableResult - public func set(responseSerializer: ResponseSerializer) -> Self{ - self.responseSerializer = responseSerializer - return self - } - - @discardableResult - public func set(completion:completionClosures?) -> Self{ - self.completion = completion - return self - } - - @discardableResult - public func add(headerKey:String, headerValue:String) -> Self { - - if (self.headers == nil){ - self.headers = [String:String]() - } - - self.headers![headerKey] = headerValue - return self - } - - @discardableResult - public func setBody(key: String, value:JSON) -> Self { - - if self.jsonBody != nil { - self.jsonBody![key] = value - }else{ - self.jsonBody = [key:value] - } - return self - } - - public func setParam(key: String, value:String) -> Self { - - if var params = self.urlParams { - params[key] = value - }else{ - self.urlParams = [key:value] - } - return self - } - - public func build() -> Request { - - var bodyData: Data? = nil - if let body = self.jsonBody { - do { - bodyData = try body.rawData() - }catch{ - - } - } - - if let params = self.urlParams, params.count > 0 { - - let urlComponents = NSURLComponents() - urlComponents.host = self.url.host - urlComponents.scheme = self.url.scheme - urlComponents.path = self.url.path - - var queryItems = [URLQueryItem]() - for (key, value) in params { - queryItems.append(URLQueryItem(name: key, value: value)) - } - - urlComponents.queryItems = queryItems - - if let url = urlComponents.url{ - self.url = url - } - - } - - return RequestElement(requestId: self.requestId, method:self.method , url: self.url, dataBody: bodyData, headers: self.headers, timeout: self.timeout, configuration: self.configuration,responseSerializer: self.responseSerializer, completion: self.completion) - } -} - - - - - - - diff --git a/Classes/Network/RequestConfiguration.swift b/Classes/Network/RequestConfiguration.swift deleted file mode 100644 index 0c7f7dcf..00000000 --- a/Classes/Network/RequestConfiguration.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// RequestConfiguration.swift -// Pods -// -// Created by Admin on 10/11/2016. -// -// - -import UIKit - - -var defaultTimeOut = 3.0 -var defaultRetryCount = 3 - -public class RequestConfiguration { - - public var readTimeOut: Double = defaultTimeOut - public var writeTimeOut: Double = defaultTimeOut - public var connectTimeOut: Double = defaultTimeOut - public var retryCount: Int = defaultRetryCount - public var ignoreLocalCache: Bool = false - - public init() { - - } -} diff --git a/Classes/Network/RequestExecutor.swift b/Classes/Network/RequestExecutor.swift deleted file mode 100644 index 692f892c..00000000 --- a/Classes/Network/RequestExecutor.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// RequestQueue.swift -// Pods -// -// Created by Admin on 10/11/2016. -// -// - -import UIKit - -// TODO: make @objc if needed in the future -public protocol RequestExecutor { - - func send(request: Request) - func cancel(request: Request) - func clean() -} diff --git a/Classes/Network/Response.swift b/Classes/Network/Response.swift deleted file mode 100644 index 1ee67b38..00000000 --- a/Classes/Network/Response.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Response.swift -// Pods -// -// Created by Admin on 10/11/2016. -// -// - -import UIKit - -public class Response: Result { - - public let statusCode: Int = 0 -} diff --git a/Classes/Network/ResponseSerializer.swift b/Classes/Network/ResponseSerializer.swift deleted file mode 100644 index c6b06567..00000000 --- a/Classes/Network/ResponseSerializer.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// ResponseSerializer.swift -// Pods -// -// Created by Rivka Peleg on 14/03/2017. -// -// - -import Foundation - -enum SerializerError: Error { - case serializationError -} - - -public protocol ResponseSerializer { - /** - This fuction will serialize the response data of certin request to the expected type according to the serializer type - */ - func serialize(data: Data) throws -> Any -} - - - class JSONSerializer: ResponseSerializer { - - func serialize(data: Data) throws -> Any { - let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions()) - return json - } -} - - - class IntSerializer: ResponseSerializer { - - func serialize(data: Data) throws -> Any { - guard let int8 = [UInt8](data).last else { - throw SerializerError.serializationError - } - - let int: Int = Int(int8) - return int - } -} - - class StringSerializer: ResponseSerializer { - - func serialize(data: Data) throws -> Any { - let string = String(data: data, encoding: .utf8) - return string - } -} diff --git a/Classes/Network/Result.swift b/Classes/Network/Result.swift deleted file mode 100644 index 512c70b2..00000000 --- a/Classes/Network/Result.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Result.swift -// Pods -// -// Created by Admin on 08/11/2016. -// -// - -import UIKit - - -public class Result: NSObject { - - public var data: T? = nil - public var error: Error? = nil - - public init(data:T?, error:Error?) { - self.data = data - self.error = error - } - - public convenience init(data: T) { - self.init(data: data, error: nil) - } - - public convenience init(error: Error) { - self.init(data: nil, error: error) - } -} diff --git a/Classes/Network/SessionProvider.swift b/Classes/Network/SessionProvider.swift deleted file mode 100644 index e2990f96..00000000 --- a/Classes/Network/SessionProvider.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// SessionProvider.swift -// Pods -// -// Created by Admin on 13/11/2016. -// -// - -import UIKit - -@objc public protocol SessionProvider: class { - - var serverURL: String { get } - var partnerId: Int64 { get } - - func loadKS(completion: @escaping (String?, Error?) -> Void) -} - - diff --git a/Classes/Network/USRExecutor.swift b/Classes/Network/USRExecutor.swift deleted file mode 100644 index 4268deba..00000000 --- a/Classes/Network/USRExecutor.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// URLSessionRequestExecutor.swift -// Pods -// -// Created by Admin on 10/11/2016. -// -// - -import UIKit - - -@objc public class USRExecutor: NSObject, RequestExecutor, URLSessionDelegate { - - var tasks: [URLSessionDataTask] = [URLSessionDataTask]() - var taskIdByRequestID: [String: Int] = [String: Int]() - - enum ResponseError: Error { - case emptyOrIncorrectURL - case incorrectJSONBody - } - - public static let shared = USRExecutor() - - public func send(request r: Request){ - - var request: URLRequest = URLRequest(url: r.url) - - //handle http method - if let method = r.method { - request.httpMethod = method.value - } - - // handle body - - if let data = r.dataBody { - request.httpBody = data - } - - // handle headers - if let headers = r.headers{ - for (headerKey,headerValue) in headers{ - request.setValue(headerValue, forHTTPHeaderField: headerKey) - } - } - - let session: URLSession! - - if let conf = r.configuration, conf.ignoreLocalCache { - let configuration = URLSessionConfiguration.default - configuration.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData - session = URLSession(configuration: configuration) - } else { - session = URLSession.shared - } - - var task: URLSessionDataTask? = nil - // settings headers: - task = session.dataTask(with: request) { (data, response, error) in - - let index = self.taskIndexForRequest(request: r) - if let i = index { - self.tasks.remove(at: i) - } - - DispatchQueue.main.async { - if let completion = r.completion { - - if let error = error as? NSError { - if error.code == NSURLErrorCancelled { - // canceled - } else { - let result = Response(data: nil, error:error) - completion(result) - // some other error - } - return - } - - if let d = data { - do { - let json = try r.responseSerializer.serialize(data: d) - let result = Response(data: json, error:nil) - completion(result) - } catch { - let result = Response(data: nil, error:error) - completion(result) - - } - } else { - let result = Response(data: nil, error:nil) - completion(result) - } - } - } - } - - if let tsk = task{ - self.taskIdByRequestID[r.requestId] = task?.taskIdentifier - self.tasks.append(tsk) - tsk.resume() - } - } - - public func cancel(request:Request){ - - let index = self.taskIndexForRequest(request: request) - if let i = index { - let task = self.tasks[i] - task.cancel() - } - } - - public func taskIndexForRequest(request:Request) -> Int?{ - - if let taskId = self.taskIdByRequestID[request.requestId]{ - - let taskIndex = self.tasks.index(where: { (taskInArray:URLSessionDataTask) -> Bool in - - if taskInArray.taskIdentifier == taskId { - return true - }else{ - return false - } - }) - - if let index = taskIndex{ - return index - } else { - return nil - } - - } else { - return nil - } - } - - public func clean(){ - - } - - // MARK: URLSessionDelegate - public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?){ - - } - - public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void){ - - } - - public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession){ - - } - -} 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/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/PKStateMachine.swift b/Classes/PKStateMachine.swift new file mode 100644 index 00000000..4cf67645 --- /dev/null +++ b/Classes/PKStateMachine.swift @@ -0,0 +1,75 @@ +// +// PKStateMachine.swift +// Pods +// +// Created by Gal Orlanczyk on 02/04/2017. +// +// + +import Foundation + +protocol IntRawRepresentable: RawRepresentable { + var rawValue: Int { get } +} + +protocol StateProtocol: IntRawRepresentable, Hashable {} + +extension StateProtocol { + var hashValue: Int { + return rawValue + } +} + +class BasicStateMachine { + /// the current state. + private var state: T + /// the queue to make changes and fetches on. + let dispatchQueue: DispatchQueue + /// the initial state of the state machine. + let initialState: T + /// indicates whether it is allowed to change the state to the initial one. + var allowTransitionToInitialState: Bool + /// a block to perform on every state changing (performed on the main queue). + var onStateChange: ((T) -> Void)? + + init(initialState: T, allowTransitionToInitialState: Bool = true) { + self.state = initialState + self.initialState = initialState + self.allowTransitionToInitialState = allowTransitionToInitialState + self.dispatchQueue = DispatchQueue(label: "com.kaltura.playkit.dispatch-queue.\(String(describing: type(of: self)))") + } + + /// gets the current state. + func getState() -> T { + return self.dispatchQueue.sync { + return self.state + } + } + + /// sets the state to a new value. + func set(state: T) { + self.dispatchQueue.sync { + if state == self.initialState && !self.allowTransitionToInitialState { + PKLog.error("\(String(describing: type(of: self))) was set to initial state, this is not allowed") + return + } + // only set state when changed + if self.state != state { + self.state = state + DispatchQueue.main.async { + self.onStateChange?(state) + } + } + } + } + + /// sets the state machine to the initial value. + func reset() { + dispatchQueue.sync { + self.state = self.initialState + DispatchQueue.main.async { + self.onStateChange?(self.state) + } + } + } +} diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift index 296c1c25..22226ca2 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift @@ -11,6 +11,15 @@ 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) + } + func asynchronouslyLoadURLAsset(_ newAsset: AVAsset) { /* Using AVAsset now runs the risk of blocking the current thread (the @@ -59,9 +68,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..3e3080d9 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) ] } @@ -36,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) @@ -67,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))) } } @@ -87,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()) } @@ -120,24 +115,13 @@ extension AVPlayerEngine { PKLog.debug("keyPath:: \(keyPath)") switch keyPath { - case #keyPath(currentItem.playbackLikelyToKeepUp): - 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): - 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) } } @@ -198,6 +182,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()) } @@ -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 c694a23b..81913f7e 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift @@ -26,6 +26,7 @@ class AVPlayerEngine: AVPlayer { private var avPlayerLayer: AVPlayerLayer! private var _view: PlayerView! + private var isDestroyed: Bool = false /// Keeps reference on the last timebase rate in order to post events accuratly. var lastTimebaseRate: Float64 = 0 @@ -159,6 +160,10 @@ class AVPlayerEngine: AVPlayer { deinit { PKLog.debug("\(String(describing: type(of: self))), was deinitialized") + // Avoid dealloc while key value observers were still registered + if (!self.isDestroyed) { + self.removeObservers() + } } func stop() { @@ -166,6 +171,7 @@ class AVPlayerEngine: AVPlayer { self.pause() self.seek(to: kCMTimeZero) self.replaceCurrentItem(with: nil) + self.post(event: PlayerEvent.Stopped()) } override func pause() { @@ -202,6 +208,7 @@ class AVPlayerEngine: AVPlayer { // removes app state observer AppStateSubject.shared.remove(observer: self) self.replaceCurrentItem(with: nil) + self.isDestroyed = true } } diff --git a/Classes/Player/AssetLoaderDelegate.swift b/Classes/Player/AssetLoaderDelegate.swift index d582b157..fab79081 100644 --- a/Classes/Player/AssetLoaderDelegate.swift +++ b/Classes/Player/AssetLoaderDelegate.swift @@ -103,7 +103,7 @@ class AssetLoaderDelegate: NSObject { return ckc } - func performCKCRequest(_ spcData: Data, _ callback: @escaping (Result)->Void) { + func performCKCRequest(_ spcData: Data, _ callback: @escaping (_ data:Data?, _ error:Error?)->Void) { guard let licenseUri = drmData?.licenseUri else { return } @@ -119,9 +119,9 @@ class AssetLoaderDelegate: NSObject { let endTime: Double = Date.timeIntervalSinceReferenceDate PKLog.debug("Got response in \(endTime-startTime) sec") let ckc = try self.parseServerResponse(data: data, error: error) - callback(Result(data: ckc)) + callback(ckc,nil) } catch let e { - callback(Result(error: e)) + callback(nil,e) } }) dataTask.resume() @@ -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 @@ -248,11 +248,11 @@ class AssetLoaderDelegate: NSObject { content key, it will honor the type of rental or lease specified when the key is used. */ - performCKCRequest(spcData) { result in - if let ckcData = result.data { + performCKCRequest(spcData) { (data,error) in + if let ckcData = data { self.handleCKCData(resourceLoadingRequest, assetIDString, ckcData) } else { - PKLog.error("Error occured while loading FairPlay license:", result.error ?? "") + PKLog.error("Error occured while loading FairPlay license:", error ?? "") } } 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/Player/Player.swift b/Classes/Player/Player.swift index e3b35e48..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/PlayerConfig.swift b/Classes/Player/PlayerConfig.swift index 77d42d04..126c6e17 100644 --- a/Classes/Player/PlayerConfig.swift +++ b/Classes/Player/PlayerConfig.swift @@ -37,6 +37,14 @@ import Foundation } } +extension MediaConfig: NSCopying { + + public func copy(with zone: NSZone? = nil) -> Any { + let copy = MediaConfig(mediaEntry: self.mediaEntry, startTime: self.startTime) + return copy + } +} + /// A `PluginConfig` object defines config to use when loading a plugin object. @objc public class PluginConfig: NSObject { /// Plugins config dictionary holds [plugin name : plugin config] @@ -59,7 +67,7 @@ import Foundation extension PluginConfig: NSCopying { public func copy(with zone: NSZone? = nil) -> Any { - let copy = PluginConfig(config: config) + let copy = PluginConfig(config: self.config) return copy } } 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 227a2f96..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/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/PlayerEvent.swift b/Classes/PlayerEvent.swift index e7ae6c5b..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,12 +162,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 + 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 - @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,12 +177,13 @@ 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 - @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 @@ -163,6 +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 @@ -172,23 +208,28 @@ import AVFoundation } } + class AdLoaded: AdEvent { + convenience init(adInfo: PKAdInfo) { + self.init([AdEventDataKeys.adInfo: adInfo]) + } + } + class AdBreakReady: AdEvent {} - class AdBreakEnded: AdEvent {} - class AdBreakStarted: AdEvent {} class AllAdsCompleted: AdEvent {} class AdComplete: AdEvent {} class AdClicked: AdEvent {} class AdFirstQuartile: AdEvent {} - class AdLoaded: AdEvent {} class AdLog: AdEvent {} class AdMidpoint: AdEvent {} class AdPaused: AdEvent {} class AdResumed: AdEvent {} class AdSkipped: AdEvent {} - class AdStreamLoaded: AdEvent {} 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) { @@ -209,8 +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!) { @@ -238,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 } @@ -266,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/Classes/Plugins/Ads/PKAdInfo.swift b/Classes/Plugins/Ads/PKAdInfo.swift index 3dad2650..d99487bd 100644 --- a/Classes/Plugins/Ads/PKAdInfo.swift +++ b/Classes/Plugins/Ads/PKAdInfo.swift @@ -28,19 +28,17 @@ import Foundation @objc public var adSystem: String @objc public var height: Int @objc public var width: Int - @objc public var podCount: Int - @objc public var podPosition: Int - /** - The position of the pod in the content in seconds. Pre-roll returns 0, - post-roll returns -1 and mid-rolls return the scheduled time of the pod. - */ - @objc public var podTimeOffset: TimeInterval + @objc public var totalAds: Int + @objc public var adPosition: Int + /// The position of the pod in the content in seconds. Pre-roll returns 0, + /// post-roll returns -1 and mid-rolls return the scheduled time of the pod. + @objc public var timeOffset: TimeInterval /// returns the position type of the ad (pre, mid, post) @objc public var positionType: AdPositionType { - if podTimeOffset > 0 { + if timeOffset > 0 { return .midRoll - } else if podTimeOffset < 0 { + } else if timeOffset < 0 { return .postRoll } else { return .preRoll @@ -56,9 +54,9 @@ import Foundation adSystem: String, height: Int, width: Int, - podCount: Int, - podPosition: Int, - podTimeOffset: TimeInterval) { + totalAds: Int, + adPosition: Int, + timeOffset: TimeInterval) { self.adDescription = adDescription self.duration = adDuration @@ -69,9 +67,9 @@ import Foundation self.adSystem = adSystem self.height = height self.width = width - self.podCount = podCount - self.podPosition = podPosition - self.podTimeOffset = podTimeOffset + self.totalAds = totalAds + self.adPosition = adPosition + self.timeOffset = timeOffset } } diff --git a/Classes/Providers/OTT/Model/OTTAsset.swift b/Classes/Providers/OTT/Model/OTTAsset.swift deleted file mode 100644 index 8f19ec63..00000000 --- a/Classes/Providers/OTT/Model/OTTAsset.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// Asset.swift -// Pods -// -// Created by Admin on 15/11/2016. -// -// - -import UIKit -import SwiftyJSON - -internal class OTTAsset: OTTBaseObject { - - var id: String - var files: [OTTFile]? - - private let idKey = "id" - private let idfiles = "mediaFiles" - - required init?(json: Any) { - - let assetJson = JSON(json) - guard let id = assetJson[idKey].number else { - return nil - } - - self.id = id.stringValue - if let jsonFiles = assetJson[idfiles].array { - - self.files = [OTTFile]() - for jsonFile in jsonFiles { - if let file = OTTFile(json: jsonFile.object) { - self.files?.append(file) - } - } - } - } -} diff --git a/Classes/Providers/OTT/Model/OTTFile.swift b/Classes/Providers/OTT/Model/OTTFile.swift deleted file mode 100644 index 698836e7..00000000 --- a/Classes/Providers/OTT/Model/OTTFile.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// File.swift -// Pods -// -// Created by Admin on 15/11/2016. -// -// - -import UIKit -import SwiftyJSON - -internal class OTTFile: OTTBaseObject { - - internal var id: String - internal var type: String? - internal var url: URL? - internal var duration: TimeInterval? - - private let idKey: String = "id" - private let typeKey: String = "type" - private let urlKey: String = "url" - private let durationKey: String = "url" - - internal init(id: String) { - self.id = id - } - - internal required init?(json:Any) { - - let fileJosn = JSON(json) - - if let id = fileJosn[idKey].number { - self.id = id.stringValue - } else { - return nil - } - - self.type = fileJosn[typeKey].string - if let contentURL = fileJosn[urlKey].string { - self.url = URL(string: contentURL) - } - self.duration = fileJosn[durationKey].number?.doubleValue - } -} diff --git a/Classes/Providers/OTT/Model/OTTGetAssetResponse.swift b/Classes/Providers/OTT/Model/OTTGetAssetResponse.swift deleted file mode 100644 index 36f8ee6d..00000000 --- a/Classes/Providers/OTT/Model/OTTGetAssetResponse.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// OTTResponse.swift -// Pods -// -// Created by Admin on 15/11/2016. -// -// - -import UIKit -import SwiftyJSON - -internal class OTTGetAssetResponse: OTTBaseObject { - - internal var asset: OTTAsset? - - private let resultKey = "result" - - internal required init(json:Any) { - - let responseJson = JSON(json) - let assetJson = responseJson[resultKey] - self.asset = OTTAsset(json: assetJson.object) - } -} diff --git a/Classes/Providers/OTT/Model/OTTLicensedURL.swift b/Classes/Providers/OTT/Model/OTTLicensedURL.swift deleted file mode 100644 index ea40ec67..00000000 --- a/Classes/Providers/OTT/Model/OTTLicensedURL.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// OTTLicensedURL.swift -// Pods -// -// Created by Admin on 21/11/2016. -// -// - -import UIKit -import SwiftyJSON - -internal class OTTLicensedURL: OTTBaseObject { - - internal var mainuRL: String - - private let mainuRLKey = "mainUrl" - private let resultKey = "result" - - internal required init?(json:Any) { - - let licensedURLJson = JSON(json) - guard let url = licensedURLJson[resultKey][mainuRLKey].string else { - return nil - } - - self.mainuRL = url - - } -} diff --git a/Classes/Providers/OTT/Model/OTTLoginResponse.swift b/Classes/Providers/OTT/Model/OTTLoginResponse.swift deleted file mode 100644 index 263147e1..00000000 --- a/Classes/Providers/OTT/Model/OTTLoginResponse.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// OTTUser.swift -// Pods -// -// Created by Admin on 17/11/2016. -// -// - -import UIKit -import SwiftyJSON - -internal class OTTLoginResponse: OTTBaseObject { - - internal var loginSession: OTTLoginSession? - - private let sessionKey = "loginSession" - - required init(json:Any) { - - let loginJsonResponse = JSON(json) - let sessionJson = loginJsonResponse[sessionKey] - self.loginSession = OTTLoginSession(json: sessionJson.object) - - } -} diff --git a/Classes/Providers/OTT/Model/OTTLoginSession.swift b/Classes/Providers/OTT/Model/OTTLoginSession.swift deleted file mode 100644 index 4dc676b0..00000000 --- a/Classes/Providers/OTT/Model/OTTLoginSession.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// OTTLoginSession.swift -// Pods -// -// Created by Rivka Peleg on 04/12/2016. -// -// - -import UIKit -import SwiftyJSON - -class OTTLoginSession: OTTBaseObject { - - internal var ks: String? - internal var refreshToken: String? - - private let ksKey = "ks" - private let refreshTokenKey = "refreshToken" - - required init(json:Any) { - - let jsonObject = JSON(json) - self.ks = jsonObject[ksKey].string - self.refreshToken = jsonObject[refreshTokenKey].string - - } -} diff --git a/Classes/Providers/OTT/Model/OTTRefreshedSession.swift b/Classes/Providers/OTT/Model/OTTRefreshedSession.swift deleted file mode 100644 index eb143544..00000000 --- a/Classes/Providers/OTT/Model/OTTRefreshedSession.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// OTTRefreshedSession.swift -// Pods -// -// Created by Rivka Peleg on 24/11/2016. -// -// - -import UIKit -import SwiftyJSON - -class OTTRefreshedSession: OTTBaseObject { - - var ks: String? - var refreshToken: String? - - private let ksKey = "ks" - private let refreshTokenKey = "refreshToken" - - required init?(json: Any) { - - let json = JSON(json) - self.ks = json[ksKey].string - self.refreshToken = json[refreshTokenKey].string - - } -} diff --git a/Classes/Providers/OTT/Model/OTTSession.swift b/Classes/Providers/OTT/Model/OTTSession.swift deleted file mode 100644 index 60240116..00000000 --- a/Classes/Providers/OTT/Model/OTTSession.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// OTTSession.swift -// Pods -// -// Created by Rivka Peleg on 24/11/2016. -// -// - -import UIKit -import SwiftyJSON - -class OTTSession: OTTBaseObject { - - var tokenExpiration: Date? - var udid: String? - - let tokenExpirationKey = "expiry" - let udidKey = "udid" - - required init?(json: Any) { - let jsonObject = JSON(json) - if let time = jsonObject[tokenExpirationKey].number?.doubleValue { - self.tokenExpiration = Date.init(timeIntervalSince1970:time) - } - - self.udid = jsonObject[udidKey].string - - } -} diff --git a/Classes/Providers/OTT/PhoenixMediaProvider.swift b/Classes/Providers/OTT/PhoenixMediaProvider.swift index ffda69c2..04c8ce62 100644 --- a/Classes/Providers/OTT/PhoenixMediaProvider.swift +++ b/Classes/Providers/OTT/PhoenixMediaProvider.swift @@ -8,16 +8,56 @@ import UIKit import SwiftyJSON +import KalturaNetKit + + +@objc public enum AssetType: Int { + case media + case epg + case unknown + + var asString: String { + switch self { + case .media: return "media" + case .epg: return "epg" + case .unknown: return "" + } + } +} + + +@objc public enum PlaybackContextType: Int { + + case trailer + case catchup + case startOver + case playback + case unknown + + var asString: String { + switch self { + case .trailer: return "TRAILER" + case .catchup: return "CATCHUP" + case .startOver: return "START_OVER" + case .playback: return "PLAYBACK" + case .unknown: return "" + } + } +} + /************************************************************/ // 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" @@ -27,6 +67,7 @@ public enum PhoenixMediaProviderError: PKError { case .unableToParseData: return 1 case .noSourcesFound: return 2 case .serverError: return 3 + case .emptyResponse: return 4 } } @@ -34,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" } } @@ -168,10 +210,10 @@ public enum PhoenixMediaProviderError: PKError { struct LoaderInfo { var sessionProvider: SessionProvider var assetId: String - var assetType: AssetType + var assetType: AssetObjectType var formats: [String]? var fileIds: [String]? - var playbackContextType: PlaybackContextType + var playbackContextType: PlaybackType var networkProtocol: String var executor: RequestExecutor @@ -198,7 +240,9 @@ public enum PhoenixMediaProviderError: PKError { let pr = self.networkProtocol ?? defaultProtocol let executor = self.executor ?? USRExecutor.shared - let loaderParams = LoaderInfo(sessionProvider: sessionProvider, assetId: assetId, assetType: self.type, formats: self.formats, fileIds: self.fileIds, playbackContextType: self.playbackContextType, networkProtocol:pr, executor: executor) + let assetType = self.convertAssetTyp(type: self.type) + let contextPlaybackContextType = self.convertPlaybackContextType(type: self.playbackContextType) + let loaderParams = LoaderInfo(sessionProvider: sessionProvider, assetId: assetId, assetType: assetType, formats: self.formats, fileIds: self.fileIds, playbackContextType: contextPlaybackContextType, networkProtocol: pr, executor: executor) self.startLoad(loaderInfo: loaderParams, callback: callback) } @@ -251,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 { @@ -276,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() @@ -389,5 +443,32 @@ public enum PhoenixMediaProviderError: PKError { return .unknown } } + + func convertAssetTyp(type: AssetType) -> AssetObjectType { + + switch type { + case .epg: + return .epg + case .media: + return .media + default: + return .unknown + } + } + + func convertPlaybackContextType(type: PlaybackContextType) -> PlaybackType { + switch type { + case .catchup: + return .catchup + case .playback: + return .playback + case .startOver: + return .startOver + case .trailer: + return .trailer + default: + return .unknown + } + } } diff --git a/Classes/Providers/OTT/Services/OTTLicensedURLService.swift b/Classes/Providers/OTT/Services/OTTLicensedURLService.swift deleted file mode 100644 index 03a343dd..00000000 --- a/Classes/Providers/OTT/Services/OTTLicensedURLService.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// LicensedURLService.swift -// Pods -// -// Created by Admin on 21/11/2016. -// -// - -import UIKit -import SwiftyJSON - -class OTTLicensedURLService: NSObject { - - internal static func get(baseURL: String, ks: String, fileId: String, fileBaseURL: String) -> KalturaRequestBuilder? { - - if let request: KalturaRequestBuilder = KalturaRequestBuilder(url: baseURL, service: "licensedUrl", action: "get") { - request.setBody(key:"ks", value: JSON(ks)) - .setBody(key: "content_id", value: JSON(fileId)) - .setBody(key: "base_url", value: JSON(fileBaseURL)) - return request - } else { - return nil - } - } - -} diff --git a/Classes/Providers/OTT/Services/OTTSessionService.swift b/Classes/Providers/OTT/Services/OTTSessionService.swift deleted file mode 100644 index bfd04165..00000000 --- a/Classes/Providers/OTT/Services/OTTSessionService.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// OTTSessionService.swift -// Pods -// -// Created by Admin on 17/11/2016. -// -// - -import UIKit -import SwiftyJSON - -internal class OTTSessionService: NSObject { - - internal static func get(baseURL: String, ks: String) -> KalturaRequestBuilder? { - - if let request = KalturaRequestBuilder(url: baseURL, service: "session", action: "get") { - request - .setBody(key: "ks", value: JSON(ks)) - return request - } else { - return nil - } - - } - - internal static func switchUser(baseURL: String, ks: String, userId: String) -> KalturaRequestBuilder? { - - if let request = KalturaRequestBuilder(url: baseURL, service: "session", action: "switchUser") { - request - .setBody(key: "ks", value: JSON(ks)) - .setBody(key: "userIdToSwitch", value: JSON(userId)) - return request - } else { - return nil - } - - } - -} diff --git a/Classes/Providers/OTT/Services/OTTSocialService.swift b/Classes/Providers/OTT/Services/OTTSocialService.swift deleted file mode 100644 index d490a8aa..00000000 --- a/Classes/Providers/OTT/Services/OTTSocialService.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// OTTSocialService.swift -// Pods -// -// Created by Rivka Peleg on 09/03/2017. -// -// - -import Foundation -import SwiftyJSON - -@objc public enum KalturaSocialNetwork: Int { - case facebook - - func stringValue() -> String { - switch self { - case .facebook: - return "FACEBOOK" - default: - return "" - } - } -} - -class OTTSocialService: NSObject { - - internal static func login(baseURL: String, partner: Int, token: String, type: KalturaSocialNetwork, udid: String) -> KalturaRequestBuilder? { - - if let request: KalturaRequestBuilder = KalturaRequestBuilder(url: baseURL, service: "social", action: "login") { - request - .setBody(key: "partnerId", value: JSON(partner)) - .setBody(key: "token", value: JSON(token)) - .setBody(key: "type", value: JSON(type.stringValue())) - .setBody(key: "udid", value:JSON(udid)) - return request - } else { - return nil - } - } - -} diff --git a/Classes/Providers/OTT/Services/OTTUserService.swift b/Classes/Providers/OTT/Services/OTTUserService.swift deleted file mode 100644 index 596cd037..00000000 --- a/Classes/Providers/OTT/Services/OTTUserService.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// OTTUserService.swift -// Pods -// -// Created by Admin on 13/11/2016. -// -// - -import UIKit -import SwiftyJSON - -public class OTTUserService: NSObject { - - internal static func login(baseURL: String, partnerId: Int64, username: String, password: String, udid: String? = nil) -> KalturaRequestBuilder? { - - if let request = KalturaRequestBuilder(url: baseURL, service: "ottUser", action: "login") { - request - .setBody(key: "username", value: JSON(username)) - .setBody(key: "password", value: JSON(password)) - .setBody(key: "partnerId", value: JSON(NSNumber.init(value: partnerId))) - - if let deviceId = udid { - request.setBody(key: "udid", value: JSON(udid)) - } - return request - } - - return nil - } - - internal static func refreshSession(baseURL: String, refreshToken: String, ks: String, udid: String? = nil) -> KalturaRequestBuilder? { - if let request = KalturaRequestBuilder(url: baseURL, service: "ottUser", action: "refreshSession") { - request - .setBody(key: "refreshToken", value: JSON(refreshToken)) - .setBody(key: "ks", value: JSON(ks)) - if let deviceId = udid { - request.setBody(key: "udid", value: JSON(udid)) - } - return request - } - return nil - } - - internal static func anonymousLogin(baseURL: String, partnerId: Int64, udid: String? = nil) -> KalturaRequestBuilder? { - if let request = KalturaRequestBuilder(url: baseURL, service: "ottUser", action: "anonymousLogin") { - request.setBody(key: "partnerId", value: JSON(NSNumber.init(value: partnerId))) - - if let deviceId = udid { - request.setBody(key: "udid", value: JSON(udid)) - } - return request - } - return nil - } - - internal static func logout(baseURL: String, partnerId: Int64, ks: String, udid: String? = nil) -> KalturaRequestBuilder? { - if let request = KalturaRequestBuilder(url: baseURL, service: "ottUser", action: "logout") { - request.setBody(key: "ks", value: JSON(ks)) - request.setBody(key: "partnerId", value: JSON(NSNumber.init(value: partnerId))) - - if let deviceId = udid { - request.setBody(key: "udid", value: JSON(udid)) - } - - return request - } - return nil - } -} diff --git a/Classes/Providers/OTT/Session/OTTSessionManager.swift b/Classes/Providers/OTT/Session/OTTSessionManager.swift deleted file mode 100644 index eaa5f26e..00000000 --- a/Classes/Providers/OTT/Session/OTTSessionManager.swift +++ /dev/null @@ -1,367 +0,0 @@ -// -// SessionManager.swift -// Pods -// -// Created by Admin on 17/11/2016. -// -// - -import UIKit - -public struct SessionInfo { - - public private(set) var udid: String? - public private(set) var ks: String? - public private(set) var refreshToken: String? - public private(set) var tokenExpiration: Date? -} - -@objc public protocol OTTSessionManagerDelegate { - func sessionManagerDidUpdateSession(sender: OTTSessionManager) -} - -@objc public class OTTSessionManager: NSObject, SessionProvider { - - enum SessionManagerError: Error { - case failed - case failedToGetLoginResponse - case failedToRefreshKS - case failedToLogout - } - - public weak var delegate: OTTSessionManagerDelegate? - public var saftyMargin: TimeInterval = 0 - - @objc public var serverURL: String - @objc public var partnerId: Int64 - - public var executor: RequestExecutor - - public private(set) var sessionInfo: SessionInfo? { - didSet { - self.delegate?.sessionManagerDidUpdateSession(sender: self) - } - } - - /************************************************************/ - // MARK: - initialization - /************************************************************/ - public init(serverURL: String, partnerId: Int64, executor: RequestExecutor?) { - self.serverURL = serverURL - self.partnerId = partnerId - if let exe = executor { - self.executor = exe - } else { - self.executor = USRExecutor.shared - } - } - - @objc public convenience init(serverURL: String, partnerId: Int64) { - self.init(serverURL: serverURL, partnerId: partnerId, executor: nil) - } - - /************************************************************/ - // MARK: - clearSessionData - /************************************************************/ - func clearSessionData() { - self.sessionInfo = SessionInfo(udid: nil, ks: nil, refreshToken: nil, tokenExpiration: nil) - } - - /************************************************************/ - // MARK: - loadKS - /************************************************************/ - @objc public func loadKS(completion: @escaping (String?, Error?) -> Void) { - - let now = Date() - if let expiration = self.sessionInfo?.tokenExpiration, expiration.timeIntervalSince(now) > saftyMargin, let ks = self.sessionInfo?.ks { - completion(ks, nil) - } else { - self.refreshKS(completion: completion) - } - } - - /************************************************************/ - // MARK: - Logout - /************************************************************/ - @objc public func logout( completion: @escaping (_ error: Error?) -> Void ) { - - guard let ks = self.sessionInfo?.ks, - let udid = self.sessionInfo?.udid else { - self.clearSessionData() - completion(nil) - return - } - - let logoutRequest = OTTUserService.logout(baseURL: self.serverURL, partnerId: self.partnerId, ks: ks, udid: udid)? - .setOTTBasicParams() - .set(completion: { (response) in - completion(response.error != nil ? SessionManagerError.failedToLogout : nil) - self.clearSessionData() - }).build() - - if let req = logoutRequest { - self.executor.send(request: req) - } else { - self.clearSessionData() - completion(SessionManagerError.failedToLogout) - } - - } - - /************************************************************/ - // MARK: - recover session - /************************************************************/ - @objc public func recoverSession(ks: String?, refreshToken: String?, udid: String?, completion: @escaping (_ error: Error?) -> Void ) { - - self.sessionInfo = SessionInfo(udid: udid, ks: ks, refreshToken: refreshToken, tokenExpiration: nil) - self.refreshKS { (_, error) in - completion(error) - } - } - - /************************************************************/ - // MARK: - start session with user name and password - /************************************************************/ - @objc public func startSession(username: String, password: String, udid: String, completion: @escaping (_ error: Error?) -> Void) { - - do { - let startSessionRequests = try self.getStartSessionWithUsernameRequestBuilder(username: username, password: password, udid: udid) - self.executeSessionRequests(request: startSessionRequests, completion: completion) - - } catch { - completion(SessionManagerError.failedToGetLoginResponse) - } - } - - func getStartSessionWithUsernameRequestBuilder(username: String, password: String, udid: String) throws -> KalturaMultiRequestBuilder { - - let loginRequestBuilder = OTTUserService.login(baseURL: self.serverURL, - partnerId: partnerId, - username: username, - password: password, - udid: udid) - - let sessionGetRequest = OTTSessionService.get(baseURL: self.serverURL, - ks:"{1:result:loginSession:ks}") - - if let req1 = loginRequestBuilder, let req2 = sessionGetRequest { - if let mrb = KalturaMultiRequestBuilder(url: self.serverURL)? - .add(request: req1) - .add(request: req2) { - return mrb - } else { - throw SessionManagerError.failed - } - } else { - throw SessionManagerError.failed - } - } - - /************************************************************/ - // MARK: - start session with token - /************************************************************/ - @objc public func startSession(token: String, type: KalturaSocialNetwork, udid: String, completion: @escaping (_ error: Error?) -> Void) { - - do { - let startSessionRequests = try self.getStartSessionWithTokenRequestBuilder(token: token, type: type, udid: udid) - self.executeSessionRequests(request: startSessionRequests, completion: completion) - - } catch { - completion(SessionManagerError.failedToGetLoginResponse) - } - - } - - func getStartSessionWithTokenRequestBuilder(token: String, type: KalturaSocialNetwork, udid: String) throws -> KalturaMultiRequestBuilder { - - let loginRequestBuilder = OTTSocialService.login(baseURL: self.serverURL, - partner: Int(partnerId), - token: token, - type: type, - udid: udid) - - let sessionGetRequest = OTTSessionService.get(baseURL: self.serverURL, - ks:"{1:result:loginSession:ks}") - - if let req1 = loginRequestBuilder, let req2 = sessionGetRequest { - if let mrb = KalturaMultiRequestBuilder(url: self.serverURL)? - .add(request: req1) - .add(request: req2) { - return mrb - } else { - throw SessionManagerError.failed - } - } else { - throw SessionManagerError.failed - } - - } - - /************************************************************/ - // MARK: - switchUser - /************************************************************/ - func getswitchUserRequestBuilder(userId: String, ks: String, udid: String) throws -> KalturaMultiRequestBuilder { - - let switchUserRequest = OTTSessionService.switchUser(baseURL: self.serverURL, ks: ks, userId: userId) - let getSessionRequest = OTTSessionService.get(baseURL: self.serverURL, ks: "{1:result:ks}") - - guard let req1 = switchUserRequest, - let req2 = getSessionRequest else { - throw SessionManagerError.failed - } - - guard let mrb: KalturaMultiRequestBuilder = (KalturaMultiRequestBuilder(url: self.serverURL)?.add(request: req1).add(request: req2)) else { - throw SessionManagerError.failed - } - - return mrb - - } - - @objc public func switchUser(userId: String, udid: String, completion: @escaping (_ error: Error?) -> Void) { - - self.loadKS { (ks, _) in - - guard let token = ks else { - completion(SessionManagerError.failedToRefreshKS) - return - } - - do { - let mbr = try self.getswitchUserRequestBuilder(userId: userId, ks: token, udid: udid) - self.executeSessionRequests(request: mbr, completion:completion) - - } catch { - completion(SessionManagerError.failed) - } - } - } - - /************************************************************/ - // MARK: - startAnonymousSession - /************************************************************/ - - func getStartAnonymousSessionRequestBuilder() throws -> KalturaMultiRequestBuilder { - let loginRequestBuilder = OTTUserService.anonymousLogin(baseURL: self.serverURL, - partnerId: self.partnerId) - let sessionGetRequest = OTTSessionService.get(baseURL: self.serverURL, ks: "{1:result:ks}") - - guard let r1 = loginRequestBuilder, let r2 = sessionGetRequest else { - throw SessionManagerError.failed - } - - guard let mrb = KalturaMultiRequestBuilder(url: self.serverURL)?.add(request: r1) - .setOTTBasicParams() - .add(request: r2) else { - throw SessionManagerError.failed - } - - return mrb - } - - @objc public func startAnonymousSession(completion:@escaping (_ error: Error?) -> Void) { - - do { - let mbr = try self.getStartAnonymousSessionRequestBuilder() - self.executeSessionRequests(request: mbr, completion:completion) - - } catch { - completion(SessionManagerError.failed) - } - } - - /************************************************************/ - // MARK: - refreshKS - /************************************************************/ - - func getRefreshKSRequestBuilder() throws -> KalturaMultiRequestBuilder { - - guard let refreshToken = self.sessionInfo?.refreshToken, let ks = self.sessionInfo?.ks, let udid = self.sessionInfo?.udid else { - throw SessionManagerError.failed - } - - let refreshSessionRequest = OTTUserService.refreshSession(baseURL: self.serverURL, refreshToken: refreshToken, ks: ks, udid: udid) - let getSessionRequest = OTTSessionService.get(baseURL: self.serverURL, ks: "{1:result:ks}") - - guard let req1 = refreshSessionRequest, let req2 = getSessionRequest else { - throw SessionManagerError.failed - } - - let mrb: KalturaMultiRequestBuilder? = (KalturaMultiRequestBuilder(url: self.serverURL)?.add(request: req1).add(request: req2)) - - guard let request = mrb else { - throw SessionManagerError.failed - } - - return request - - } - - @objc public func refreshKS(completion: @escaping (String?, Error?) -> Void) { - - do { - let mbr = try self.getRefreshKSRequestBuilder() - self.executeSessionRequests(request: mbr, completion: { (error) in - if( error == nil ) { - completion(self.sessionInfo?.ks, nil) - } else { - self.clearSessionData() - completion(nil, SessionManagerError.failedToRefreshKS) - } - }) - - } catch { - self.clearSessionData() - completion(nil, SessionManagerError.failedToRefreshKS) - - } - - } - - /************************************************************/ - // MARK: - execute all session request and parse them - /************************************************************/ - private func executeSessionRequests(request: KalturaMultiRequestBuilder, completion: @escaping (_ error: Error?) -> Void) { - - request.setOTTBasicParams() - request.set(completion: { (r: Response) in - - if let data = r.data { - var result: [OTTBaseObject]? = nil - do { - result = try OTTMultiResponseParser.parse(data:data) - } catch { - completion(error) - } - - if let result = result, result.count == 2 { - let loginResult: OTTBaseObject = result[0] - let sessionResult: OTTBaseObject = result[1] - - if let loginObj = loginResult as? OTTLoginResponse, - let sessionObj = sessionResult as? OTTSession { - - self.sessionInfo = SessionInfo(udid: sessionObj.udid, ks: loginObj.loginSession?.ks, refreshToken: loginObj.loginSession?.refreshToken, tokenExpiration: sessionObj.tokenExpiration) - completion(nil) - } else if let loginObj = loginResult as? OTTLoginSession, - let sessionObj = sessionResult as? OTTSession { - - self.sessionInfo = SessionInfo(udid: sessionObj.udid, ks: loginObj.ks, refreshToken: loginObj.refreshToken, tokenExpiration: sessionObj.tokenExpiration) - completion(nil) - } else { - completion(SessionManagerError.failed) - } - - } else { - completion(SessionManagerError.failed) - } - } else { - completion(SessionManagerError.failed) - } - }) - - let request = request.build() - self.executor.send(request: request) - } - -} diff --git a/Classes/Providers/OVP/Model/OVPKS.swift b/Classes/Providers/OVP/Model/OVPKS.swift deleted file mode 100644 index 68301566..00000000 --- a/Classes/Providers/OVP/Model/OVPKS.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// OVPKS.swift -// Pods -// -// Created by Rivka Peleg on 01/01/2017. -// -// - -import UIKit - -class OVPKS: OVPBaseObject { - - var ks: String - - required init?(json: Any) { - if let ks = json as? String { - self.ks = ks - }else{ - return nil - } - } -} diff --git a/Classes/Providers/OVP/Model/OVPKalturaSessionInfo.swift b/Classes/Providers/OVP/Model/OVPKalturaSessionInfo.swift deleted file mode 100644 index 3a8d5983..00000000 --- a/Classes/Providers/OVP/Model/OVPKalturaSessionInfo.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// OVPKalturaSessionInfo.swift -// Pods -// -// Created by Rivka Peleg on 01/01/2017. -// -// - -import UIKit -import SwiftyJSON - - -//"sessionType": "2", -//"partnerId": "1851571", -//"userId": "kaltura.fe@icloud.com", -//"expiry": "1483311163", -//"privileges": "*", -//"objectType": "KalturaSessionInfo" - -class OVPKalturaSessionInfo: OVPBaseObject { - - let expiry: Date - - private let expiryKey = "expiry" - required init?(json: Any) { - - let jsonObject = JSON(json) - if let expiry = jsonObject[self.expiryKey].string, - let doubleExpiry = Double(expiry) - { - self.expiry = Date(timeIntervalSince1970: TimeInterval(doubleExpiry)) - }else{ - return nil - } - } - - -} diff --git a/Classes/Providers/OVP/OVPMediaProvider.swift b/Classes/Providers/OVP/OVPMediaProvider.swift index 5cefa1b2..3fd7d5f1 100644 --- a/Classes/Providers/OVP/OVPMediaProvider.swift +++ b/Classes/Providers/OVP/OVPMediaProvider.swift @@ -8,6 +8,7 @@ import UIKit import SwiftyXMLParser +import KalturaNetKit @objc public class OVPMediaProvider: NSObject, MediaEntryProvider { @@ -301,7 +302,6 @@ import SwiftyXMLParser .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/Parsers/OVPResponseParser.swift b/Classes/Providers/OVP/Parsers/OVPResponseParser.swift deleted file mode 100644 index d44b1484..00000000 --- a/Classes/Providers/OVP/Parsers/OVPResponseParser.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// OTTRequestParser.swift -// Pods -// -// Created by Admin on 23/11/2016. -// -// - -import UIKit -import SwiftyJSON - -class OVPResponseParser: ResponseParser { - - enum error: Error { - case typeNotFound - case invalidJsonObject - } - - static func parse(data:Any) throws -> OVPBaseObject { - - let jsonResponse = JSON(data) - let resultObjectJSON = jsonResponse.dictionaryObject - let objectType: OVPBaseObject.Type? = OVPObjectMapper.classByJsonObject(json: resultObjectJSON) - if let type = objectType{ - if let object = type.init(json: resultObjectJSON) { - return object - } else { - throw error.invalidJsonObject - } - } else { - throw error.typeNotFound - } - } - - - static func parse(data:Any) throws -> T? { - - let jsonResponse = JSON(data) - let resultObjectJSON = jsonResponse.dictionaryObject - - if let type = T.self as? OVPBaseObject.Type { - if let object = type.init(json: resultObjectJSON) { - if let result = object as? T { - return result - } else { - return nil - } - } else { - throw error.invalidJsonObject - } - } else { - throw error.typeNotFound - } - } -} - - - diff --git a/Classes/Providers/OVP/Services/OVPUserService.swift b/Classes/Providers/OVP/Services/OVPUserService.swift deleted file mode 100644 index 9d8f3cab..00000000 --- a/Classes/Providers/OVP/Services/OVPUserService.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// OVPUserService.swift -// Pods -// -// Created by Rivka Peleg on 29/12/2016. -// -// - -import UIKit -import SwiftyJSON - -class OVPUserService { - - internal static func loginByLoginId(baseURL: String, - loginId: String, - password: String, - partnerId: Int64) -> KalturaRequestBuilder? { - - if let request: KalturaRequestBuilder = KalturaRequestBuilder(url: baseURL, - service: "user", - action: "loginByLoginId") { - request.setBody(key: "loginId", value: JSON(loginId)) - .setBody(key: "password", value: JSON(password)) - .setBody(key: "partnerId", value: JSON(partnerId)) - return request - }else{ - return nil - } - } - -} diff --git a/Classes/Providers/OVP/Session/OVPSessionManager.swift b/Classes/Providers/OVP/Session/OVPSessionManager.swift deleted file mode 100644 index 36f9d4db..00000000 --- a/Classes/Providers/OVP/Session/OVPSessionManager.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// OVPSessionManager.swift -// Pods -// -// Created by Rivka Peleg on 29/12/2016. -// -// - -import UIKit - -@objc public class OVPSessionManager: NSObject, SessionProvider { - - public enum SessionManagerError: Error{ - case failedToGetKS - case failedToGetLoginResponse - case failedToRefreshKS - case failedToBuildRefreshRequest - case invalidRefreshCallResponse - case noRefreshTokenOrTokenToRefresh - case failedToParseResponse - case ksExpired - } - - @objc public var serverURL: String - @objc public var partnerId: Int64 - - private var executor: RequestExecutor - private var version: String - private var fullServerPath: String - - private var ks: String? = nil - private var tokenExpiration: Date? - - private var username: String? - private var password: String? - - private let defaultSessionExpiry = TimeInterval(24*60*60) - - public init(serverURL: String, partnerId: Int64, executor: RequestExecutor?) { - self.serverURL = serverURL - self.partnerId = partnerId - self.version = "api_v3" - self.fullServerPath = self.serverURL.appending("/\(self.version)") - - if let exe = executor { - self.executor = exe - } else { - self.executor = USRExecutor.shared - } - } - - @objc public convenience init(serverURL: String, partnerId: Int64) { - self.init(serverURL: serverURL, partnerId: partnerId, executor: nil) - } - - @available(*, deprecated, message: "Use init(serverURL:partnerId:executor:)") - public convenience init(serverURL: String, version: String, partnerId: Int64, executor: RequestExecutor?) { - self.init(serverURL: serverURL, partnerId: partnerId, executor: executor) - } - - public func loadKS(completion: @escaping (String?, Error?) -> Void){ - if let ks = self.ks, self.tokenExpiration?.compare(Date()) == ComparisonResult.orderedDescending { - completion(ks, nil) - } else { - - self.ks = nil - if let username = self.username, - let password = self.password { - - self.startSession(username: username, - password: password, completion: { (e:Error?) in - self.ensureKSAfterRefresh(e: e, completion: completion) - }) - } - else { - - self.startAnonymousSession(completion: { (e:Error?) in - self.ensureKSAfterRefresh(e: e, completion: completion) - }) - } - } - } - - - func ensureKSAfterRefresh(e:Error?,completion: @escaping (String?, Error?) -> Void) -> Void { - if let ks = self.ks { - completion(ks, nil) - } else if let error = e { - completion(nil, error) - } else { - completion(nil, SessionManagerError.ksExpired) - } - } - - - public func startAnonymousSession(completion:@escaping (_ error: Error?) -> Void) -> Void { - - let loginRequestBuilder = OVPSessionService.startWidgetSession(baseURL: self.fullServerPath, partnerId: self.partnerId)? - .setOVPBasicParams() - .set(completion: { (r:Response) in - - if let data = r.data { - var result: OVPBaseObject? = nil - do { - result = try OVPResponseParser.parse(data:data) - if let widgetSession = result as? OVPStartWidgetSessionResponse { - self.ks = widgetSession.ks - self.tokenExpiration = Date(timeIntervalSinceNow:self.defaultSessionExpiry ) - completion(nil) - - }else{ - completion(SessionManagerError.failedToGetKS) - } - - }catch{ - completion(error) - } - }else{ - completion(SessionManagerError.failedToGetLoginResponse) - } - }) - - if let request = loginRequestBuilder?.build() { - self.executor.send(request: request) - } - } - - - - public func startSession(username: String, password: String, completion: @escaping (_ error: Error?) -> Void) -> Void { - - self.username = username - self.password = password - - let loginRequestBuilder = OVPUserService.loginByLoginId(baseURL: self.fullServerPath, - loginId: username, - password: password, - partnerId: self.partnerId) - - let sessionGetRequest = OVPSessionService.get(baseURL: self.fullServerPath, - ks:"{1:result}") - - if let r1 = loginRequestBuilder, let r2 = sessionGetRequest { - - let mrb = KalturaMultiRequestBuilder(url: self.fullServerPath)?.add(request: r1).add(request: r2).setOVPBasicParams() - mrb?.set(completion: { (r:Response) in - - if let data = r.data - { - do { - guard let arrayResult = data as? [Any], - arrayResult.count == 2 - else { - completion(SessionManagerError.failedToParseResponse) - return - } - - let sessionInfo = OVPKalturaSessionInfo(json: arrayResult[1]) - self.ks = arrayResult[0] as? String - self.tokenExpiration = sessionInfo?.expiry - completion(nil) - } catch { - completion(error) - } - - } else { - completion(SessionManagerError.failedToGetLoginResponse) - } - }) - - - if let request = mrb?.build() { - self.executor.send(request: request) - } - } - } -} diff --git a/Classes/Providers/OVP/SimpleOVPSessionProvider.swift b/Classes/Providers/OVP/SimpleOVPSessionProvider.swift index 064f3f5c..4415d304 100644 --- a/Classes/Providers/OVP/SimpleOVPSessionProvider.swift +++ b/Classes/Providers/OVP/SimpleOVPSessionProvider.swift @@ -4,6 +4,7 @@ // import UIKit +import KalturaNetKit /** A SessionProvider that just reflects its input parameters -- baseUrl, partnerId, 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)) } diff --git a/Example/PlayKit.xcodeproj/project.pbxproj b/Example/PlayKit.xcodeproj/project.pbxproj index b51da04c..2c1ce687 100644 --- a/Example/PlayKit.xcodeproj/project.pbxproj +++ b/Example/PlayKit.xcodeproj/project.pbxproj @@ -24,7 +24,6 @@ C23A62D11DF413FF00635FA2 /* MockMediaProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62C91DF412FD00635FA2 /* MockMediaProviderTest.swift */; }; C23A62D21DF4140B00635FA2 /* PhoenixMediaProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62CA1DF412FD00635FA2 /* PhoenixMediaProviderTest.swift */; }; C23A62D41DF4141000635FA2 /* MediaEntryProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62CF1DF4131700635FA2 /* MediaEntryProviderMock.swift */; }; - C23A62DD1DF47D9C00635FA2 /* OTTSessionProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62DB1DF47D9800635FA2 /* OTTSessionProviderTest.swift */; }; C24770AD1DEDE72F00E37C89 /* ovp.multirequest._.1_1h1vsv3z.json in Resources */ = {isa = PBXBuildFile; fileRef = C24770AC1DEDE72F00E37C89 /* ovp.multirequest._.1_1h1vsv3z.json */; }; C24770B01DEDF36900E37C89 /* ovp.multirequest._.1_1h1vsv3z.json in Resources */ = {isa = PBXBuildFile; fileRef = C24770AC1DEDE72F00E37C89 /* ovp.multirequest._.1_1h1vsv3z.json */; }; C2D803211E6C091600A3DE15 /* OVPMediaProviederTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62CB1DF412FD00635FA2 /* OVPMediaProviederTest.swift */; }; @@ -72,7 +71,6 @@ C23A62CA1DF412FD00635FA2 /* PhoenixMediaProviderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhoenixMediaProviderTest.swift; sourceTree = ""; }; C23A62CB1DF412FD00635FA2 /* OVPMediaProviederTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OVPMediaProviederTest.swift; sourceTree = ""; }; C23A62CF1DF4131700635FA2 /* MediaEntryProviderMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaEntryProviderMock.swift; sourceTree = ""; }; - C23A62DB1DF47D9800635FA2 /* OTTSessionProviderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTTSessionProviderTest.swift; sourceTree = ""; }; C24770AC1DEDE72F00E37C89 /* ovp.multirequest._.1_1h1vsv3z.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ovp.multirequest._.1_1h1vsv3z.json; sourceTree = ""; }; C63FA2AE1DF3F854004030E0 /* PlayerControllerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerControllerTest.swift; sourceTree = ""; }; FB09C99B1E28072900D3671F /* SourceSelectorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SourceSelectorTest.swift; sourceTree = ""; }; @@ -240,7 +238,6 @@ C23A62C81DF412FD00635FA2 /* MediaEntryProvider */ = { isa = PBXGroup; children = ( - C23A62DB1DF47D9800635FA2 /* OTTSessionProviderTest.swift */, C23A62C91DF412FD00635FA2 /* MockMediaProviderTest.swift */, C23A62CA1DF412FD00635FA2 /* PhoenixMediaProviderTest.swift */, C23A62CB1DF412FD00635FA2 /* OVPMediaProviederTest.swift */, @@ -485,7 +482,6 @@ 2012AFB41E4B872300BBA61C /* PlayerCreator.swift in Sources */, C23A62D21DF4140B00635FA2 /* PhoenixMediaProviderTest.swift in Sources */, 2012AFAF1E4B85CA00BBA61C /* OTTAnalyticsPluginTest.swift in Sources */, - C23A62DD1DF47D9C00635FA2 /* OTTSessionProviderTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/PlayKit/Info.plist b/Example/PlayKit/Info.plist index df95e98c..3ac21c8e 100644 --- a/Example/PlayKit/Info.plist +++ b/Example/PlayKit/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.1.30 + 0.2.0 CFBundleSignature ???? CFBundleVersion diff --git a/Example/Podfile.lock b/Example/Podfile.lock index e1dc9efe..136e3bd1 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -3,6 +3,10 @@ PODS: - google-cast-sdk/Core (= 3.3.0) - google-cast-sdk/Core (3.3.0) - GoogleAds-IMA-iOS-SDK (3.4.1) + - KalturaNetKit (0.0.11): + - KalturaNetKit/Core (= 0.0.11) + - KalturaNetKit/Core (0.0.11): + - SwiftyJSON - Log (1.0) - Nimble (6.0.1) - PlayKit (0.1.x-dev): @@ -10,6 +14,7 @@ PODS: - PlayKit/AnalyticsCommon (0.1.x-dev): - PlayKit/Core - PlayKit/Core (0.1.x-dev): + - KalturaNetKit - Log - SwiftyJSON - SwiftyXMLParser @@ -58,9 +63,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: google-cast-sdk: da1989cbc1b9ff7b50ddb9dae5b1969d95a65a0f GoogleAds-IMA-iOS-SDK: 7355db22ce69be4607ed0cc5112847b2b0c5e89d + KalturaNetKit: b6bc21d1ffb6957192c33afc12f5c2203d5208b2 Log: 5e368c9528db07517d18d2d04ff5fe2b6f5a1e21 Nimble: 1527fd1bd2b4cf0636251a36bc8ab37e81da8347 - PlayKit: 7b8c3120a642ceda0193a3ec8f14247d20562e39 + PlayKit: c50b4b7582aa7bef031bb34e15b6ca2303093525 Quick: dafc587e21eed9f4cab3249b9f9015b0b7a7f71d SwiftyJSON: c2842d878f95482ffceec5709abc3d05680c0220 SwiftyXMLParser: 8d2295fb4fbc6e2ff241e7c8d7717e159be35969 diff --git a/Example/Tests/MediaEntryProvider/MediaEntryProviderMock.swift b/Example/Tests/MediaEntryProvider/MediaEntryProviderMock.swift index 738adc16..bbb97c46 100644 --- a/Example/Tests/MediaEntryProvider/MediaEntryProviderMock.swift +++ b/Example/Tests/MediaEntryProvider/MediaEntryProviderMock.swift @@ -8,6 +8,7 @@ import UIKit import PlayKit +import KalturaNetKit diff --git a/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift b/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift deleted file mode 100644 index 82c44ae7..00000000 --- a/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// OTTSessionProviderTest.swift -// PlayKit -// -// Created by Rivka Peleg on 04/12/2016. -// Copyright © 2016 CocoaPods. All rights reserved. -// - -import XCTest -import PlayKit - -class OTTSessionProviderTest: XCTestCase { - - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - - func testOTTSessionProvider() { - - let sessionProvider = OTTSessionManager(serverURL:"http://52.210.223.65:8080/v4_2/api_v3", partnerId:198, executor: nil) - sessionProvider.startAnonymousSession { (e:Error?) in - if e == nil{ - sessionProvider.loadKS(completion: { (ks, error) in - print(ks ?? "") - }) - }else{ - - } - - } - } -} diff --git a/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift b/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift index 85724b26..824e51a1 100644 --- a/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift +++ b/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift @@ -8,6 +8,7 @@ import XCTest import PlayKit +import KalturaNetKit diff --git a/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift b/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift index 6c3252b4..6d20a19d 100644 --- a/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift +++ b/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift @@ -8,6 +8,7 @@ import XCTest import PlayKit +import KalturaNetKit @@ -40,8 +41,8 @@ class PhoenixMediaProviderTest: XCTestCase, SessionProvider { let provider = PhoenixMediaProvider() .set(sessionProvider: self) .set(assetId: mediaID) - .set(type: AssetType.media) - .set(playbackContextType: PlaybackContextType.playback) + .set(type: .media) + .set(playbackContextType: .playback) provider.loadMedia { (entry, error) in diff --git a/PlayKit.podspec b/PlayKit.podspec index 3c15d6f8..5919d7e1 100644 --- a/PlayKit.podspec +++ b/PlayKit.podspec @@ -1,9 +1,8 @@ Pod::Spec.new do |s| s.name = 'PlayKit' -s.version = '0.1.30' +s.version = '0.2.0' s.summary = 'PlayKit: Kaltura Mobile Player SDK - iOS' - s.homepage = 'https://github.com/kaltura/playkit-ios' s.license = { :type => 'AGPLv3', :text => 'AGPLv3' } s.author = { 'Kaltura' => 'community@kaltura.com' } @@ -13,9 +12,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 'SwiftyJSON', '3.1.4' + sp.dependency 'Log', '1.0' + sp.dependency 'SwiftyXMLParser', '3.0.0' + sp.dependency 'KalturaNetKit', '~> 0.0' end s.subspec 'IMAPlugin' do |ssp| @@ -50,7 +50,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 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/IMA/AdsEnabledPlayerController.swift b/Plugins/IMA/AdsEnabledPlayerController.swift index 1ec235e3..7d7e2cbb 100644 --- a/Plugins/IMA/AdsEnabledPlayerController.swift +++ b/Plugins/IMA/AdsEnabledPlayerController.swift @@ -11,10 +11,36 @@ import UIKit import AVFoundation import AVKit +/// `AdsPlayerState` represents `AdsEnabledPlayerController` state machine states. +enum AdsPlayerState: Int, StateProtocol { + /// initial state. + case start = 0 + /// when prepare was requested for the first time and it is stalled until ad started (preroll) / faliure or content resume + case waitingForPrepare + /// a moment before we called prepare until prepare() was finished (the sychornos code only not async tasks) + case preparing + /// Indicates when prepare() was finished (the sychornos code only not async tasks) + case prepared +} + class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPluginDataSource { - var isAdPlayback = false - var isPlayEnabled = false + enum PlayType { + case play, resume + } + + /// The ads player state machine. + private var stateMachine = BasicStateMachine(initialState: AdsPlayerState.start, allowTransitionToInitialState: true) + + /// The media config to prepare the player with. + /// Uses @NSCopying in order to make a copy whenever set with new value. + @NSCopying private var prepareMediaConfig: MediaConfig! + + /// indicates if play was used, if `play()` or `resume()` was called we set this to true. + private var isPlayEnabled = false + + /// a semaphore to make sure prepare calling will not be reached from 2 threads by mistake. + private let prepareSemaphore = DispatchSemaphore(value: 1) /// when playing post roll google sends content resume when finished. /// In our case we need to prevent sending play/resume to the player because the content already ended. @@ -26,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? { @@ -37,86 +64,105 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl override var isPlaying: Bool { get { - if isAdPlayback { + if self.adsPlugin.isAdPlaying { return isPlayEnabled } return super.isPlaying } } + override func prepare(_ config: MediaConfig) { + self.stop() + self.stateMachine.set(state: .waitingForPrepare) + self.prepareMediaConfig = config + self.adsPlugin.requestAds() + } + override func play() { self.isPlayEnabled = true - if !self.adsPlugin.start(showLoadingView: true) { - super.play() - } + self.adsPlugin.didRequestPlay(ofType: .play) + } + + override func resume() { + self.isPlayEnabled = true + self.adsPlugin.didRequestPlay(ofType: .resume) } override func pause() { self.isPlayEnabled = false - if isAdPlayback { + if self.adsPlugin.isAdPlaying { self.adsPlugin.pause() } else { super.pause() } } - override func resume() { - self.isPlayEnabled = true - if isAdPlayback { - self.adsPlugin.resume() - } else { - super.resume() - } - } - override func stop() { - self.adsPlugin.destroyManager() + self.stateMachine.set(state: .start) super.stop() - self.isAdPlayback = false + self.adsPlugin.destroyManager() self.isPlayEnabled = false self.shouldPreventContentResume = false } - // TODO:: finilize prepare - override func prepare(_ config: MediaConfig) { - super.prepare(config) - self.adsPlugin.requestAds() - } - @available(iOS 9.0, *) override func createPiPController(with delegate: AVPictureInPictureControllerDelegate) -> AVPictureInPictureController? { self.adsPlugin.pipDelegate = delegate return super.createPiPController(with: self.adsPlugin) } + override func destroy() { + AppStateSubject.shared.remove(observer: self) + super.destroy() + } + + /************************************************************/ + // MARK: - AdsPluginDataSource + /************************************************************/ func adsPluginShouldPlayAd(_ adsPlugin: AdsPlugin) -> Bool { return self.delegate!.playerShouldPlayAd(self) } + var adsPluginStartTime: TimeInterval { + return self.prepareMediaConfig?.startTime ?? 0 + } + + /************************************************************/ + // MARK: - AdsPluginDelegate + /************************************************************/ + func adsPlugin(_ adsPlugin: AdsPlugin, loaderFailedWith error: String) { if self.isPlayEnabled { + self.preparePlayerIfNeeded() super.play() + self.adsPlugin.didPlay() } } func adsPlugin(_ adsPlugin: AdsPlugin, managerFailedWith error: String) { + self.preparePlayerIfNeeded() super.play() - self.isAdPlayback = false + self.adsPlugin.didPlay() } func adsPlugin(_ adsPlugin: AdsPlugin, didReceive event: PKEvent) { switch event { - case let e where type(of: e) == AdEvent.adDidRequestPause: - self.isAdPlayback = true + case let e where type(of: e) == AdEvent.adDidRequestContentPause: super.pause() - case let e where type(of: e) == AdEvent.adDidRequestResume: - self.isAdPlayback = false + case let e where type(of: e) == AdEvent.adDidRequestContentResume: if !self.shouldPreventContentResume { + self.preparePlayerIfNeeded() super.resume() } case let e where type(of: e) == AdEvent.adResumed: self.isPlayEnabled = true case let e where type(of: e) == AdEvent.adStarted: + // when starting to play pre roll start preparing the player. + if event.adInfo?.positionType == .preRoll { + self.preparePlayerIfNeeded() + } + case let e where type(of: e) == AdEvent.adLoaded || type(of: e) == AdEvent.adBreakReady: + if self.shouldPreventContentResume == true { return } // no need to handle twice if already true if event.adInfo?.positionType == .postRoll { self.shouldPreventContentResume = true } @@ -124,4 +170,49 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl default: break } } + + func adsRequestTimedOut(shouldPlay: Bool) { + if shouldPlay { + self.preparePlayerIfNeeded() + self.play() + } + } + + func play(_ playType: PlayType) { + self.preparePlayerIfNeeded() + playType == .play ? super.play() : super.resume() + self.adsPlugin.didPlay() + } + + /************************************************************/ + // MARK: - Private + /************************************************************/ + + /// prepare the player only if wasn't prepared yet. + private func preparePlayerIfNeeded() { + self.prepareSemaphore.wait() // use semaphore to make sure will not be called from more than one thread by mistake. + if self.stateMachine.getState() == .waitingForPrepare { + self.stateMachine.set(state: .preparing) + PKLog.debug("will prepare player") + super.prepare(self.prepareMediaConfig) + self.stateMachine.set(state: .prepared) + } + 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/AdsPlugin.swift b/Plugins/IMA/AdsPlugin.swift index 2583bf49..3643bee2 100644 --- a/Plugins/IMA/AdsPlugin.swift +++ b/Plugins/IMA/AdsPlugin.swift @@ -11,24 +11,41 @@ import AVKit protocol AdsPluginDataSource : class { func adsPluginShouldPlayAd(_ adsPlugin: AdsPlugin) -> Bool + /// the player's media config start time. + var adsPluginStartTime: TimeInterval { get } } protocol AdsPluginDelegate : class { func adsPlugin(_ adsPlugin: AdsPlugin, loaderFailedWith error: String) func adsPlugin(_ adsPlugin: AdsPlugin, managerFailedWith error: String) func adsPlugin(_ adsPlugin: AdsPlugin, didReceive event: PKEvent) + /// called when ads request was timed out, telling the player if it should start play afterwards. + func adsRequestTimedOut(shouldPlay: Bool) + /// called when the plugin wants the player to start play. + func play(_ playType: AdsEnabledPlayerController.PlayType) } protocol AdsPlugin: PKPlugin, AVPictureInPictureControllerDelegate { - var dataSource: AdsPluginDataSource? { get set } - var delegate: AdsPluginDelegate? { get set } + weak var dataSource: AdsPluginDataSource? { get set } + weak var delegate: AdsPluginDelegate? { get set } var pipDelegate: AVPictureInPictureControllerDelegate? { get set } + /// is ad playing currently. + var isAdPlaying: Bool { get } + /// request ads from the server. func requestAds() - func start(showLoadingView: Bool) -> Bool + /// resume ad func resume() + /// pause ad func pause() + /// ad content complete func contentComplete() + /// destroy the ads manager func destroyManager() + /// called after player called `super.play()` + func didPlay() + /// called when play() or resume() was called. + /// used to make the neccery checks with the ads plugin if can play or resume the content. + func didRequestPlay(ofType type: AdsEnabledPlayerController.PlayType) } diff --git a/Plugins/IMA/AdsConfig.swift b/Plugins/IMA/IMAConfig.swift similarity index 57% rename from Plugins/IMA/AdsConfig.swift rename to Plugins/IMA/IMAConfig.swift index 0950e558..d90886ee 100644 --- a/Plugins/IMA/AdsConfig.swift +++ b/Plugins/IMA/IMAConfig.swift @@ -9,20 +9,26 @@ import Foundation import GoogleInteractiveMediaAds -@objc public class AdsConfig: NSObject { +@objc public class IMAConfig: NSObject { + + @objc public let enableBackgroundPlayback = true + // defaulted to false, because otherwise ad breaks events will not happen. + // we need to have control on whether ad break will start playing or not using `Loaded` event is not enough. + // (will also need more safety checks for loaded because loaded will happen more than once). + @objc public let autoPlayAdBreaks = false @objc public var language: String = "en" - @objc public var enableBackgroundPlayback: Bool { - return true - } - @objc public var autoPlayAdBreaks: Bool { - return false - } + @objc public var videoBitrate = kIMAAutodetectBitrate @objc public var videoMimeTypes: [Any]? - @objc public var adTagUrl: String? + @objc public var adTagUrl: String = "" @objc public var companionView: UIView? @objc public var webOpenerPresentingController: UIViewController? - + /// ads request timeout interval, when ads request will take more then this time will resume content. + @objc public var requestTimeoutInterval: TimeInterval = IMAPlugin.defaultTimeoutInterval + /// enables debug mode on IMA SDK which will output detailed log information to the console. + /// The default value is false. + @objc public var enableDebugMode: Bool = false + // Builders @discardableResult @nonobjc public func set(language: String) -> Self { @@ -59,4 +65,10 @@ import GoogleInteractiveMediaAds self.webOpenerPresentingController = webOpenerPresentingController return self } + + @discardableResult + @nonobjc public func set(requestTimeoutInterval: TimeInterval) -> Self { + self.requestTimeoutInterval = requestTimeoutInterval + return self + } } diff --git a/Plugins/IMA/IMAExtensions.swift b/Plugins/IMA/IMAExtensions.swift new file mode 100644 index 00000000..099748ea --- /dev/null +++ b/Plugins/IMA/IMAExtensions.swift @@ -0,0 +1,36 @@ +// +// IMAExtensions.swift +// Pods +// +// Created by Gal Orlanczyk on 17/04/2017. +// +// + +import Foundation + +import GoogleInteractiveMediaAds + +extension IMAAdsManager { + func getAdCuePoints() -> PKAdCuePoints { + return PKAdCuePoints(cuePoints: self.adCuePoints as? [TimeInterval] ?? []) + } +} + +extension PKAdInfo { + convenience init(ad: IMAAd) { + self.init( + adDescription: ad.adDescription, + adDuration: ad.duration, + title: ad.adTitle, + isSkippable: ad.isSkippable, + contentType: ad.contentType, + adId: ad.adId, + adSystem: ad.adSystem, + height: Int(ad.height), + width: Int(ad.width), + totalAds: Int(ad.adPodInfo.totalAds), + adPosition: Int(ad.adPodInfo.adPosition), + timeOffset: ad.adPodInfo.timeOffset + ) + } +} diff --git a/Plugins/IMA/IMAPlugin.swift b/Plugins/IMA/IMAPlugin.swift index 539215af..73fb99c7 100644 --- a/Plugins/IMA/IMAPlugin.swift +++ b/Plugins/IMA/IMAPlugin.swift @@ -8,33 +8,33 @@ import GoogleInteractiveMediaAds -extension IMAAdsManager { - func getAdCuePoints() -> PKAdCuePoints { - return PKAdCuePoints(cuePoints: self.adCuePoints as? [TimeInterval] ?? []) - } -} - -extension PKAdInfo { - convenience init(ad: IMAAd) { - self.init( - adDescription: ad.adDescription, - adDuration: ad.duration, - title: ad.adTitle, - isSkippable: ad.isSkippable, - contentType: ad.contentType, - adId: ad.adId, - adSystem: ad.adSystem, - height: Int(ad.height), - width: Int(ad.width), - podCount: Int(ad.adPodInfo.totalAds), - podPosition: Int(ad.adPodInfo.adPosition), - podTimeOffset: ad.adPodInfo.timeOffset - ) - } +/// `IMAState` represents `IMAPlugin` state machine states. +enum IMAState: Int, StateProtocol { + /// initial state. + case start = 0 + /// ads request was made. + case adsRequested + /// ads request was made and play() was used. + case adsRequestedAndPlay + /// the ads request failed (loader failed to load ads and error was sent) + case adsRequestFailed + /// the ads request was timed out. + case adsRequestTimedOut + /// ads request was succeeded and loaded. + case adsLoaded + /// ads request was succeeded and loaded and play() was used. + case adsLoadedAndPlay + /// ads are playing. + case adsPlaying + /// content is playing. + case contentPlaying } @objc public class IMAPlugin: BasePlugin, PKPluginWarmUp, PlayerDecoratorProvider, AdsPlugin, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMAWebOpenerDelegate, IMAContentPlayhead { + /// the default timeout interval for ads request. + static let defaultTimeoutInterval: TimeInterval = 5 + weak var dataSource: AdsPluginDataSource? { didSet { PKLog.debug("data source set") @@ -43,19 +43,23 @@ extension PKAdInfo { weak var delegate: AdsPluginDelegate? weak var pipDelegate: AVPictureInPictureControllerDelegate? + /// The IMA plugin state machine + private var stateMachine = BasicStateMachine(initialState: IMAState.start, allowTransitionToInitialState: false) + + private static var loader: IMAAdsLoader! private var adsManager: IMAAdsManager? private var renderingSettings: IMAAdsRenderingSettings! = IMAAdsRenderingSettings() - private static var loader: IMAAdsLoader! - private var pictureInPictureProxy: IMAPictureInPictureProxy? private var loadingView: UIView? - // we must have config error will be thrown otherwise - private var config: AdsConfig! - private var isAdPlayback = false - private var startAdCalled = false - private var loaderFailed = false + // we must have config error will be thrown otherwise + private var config: IMAConfig! + /// timer for checking IMA requests timeout. + private var requestTimeoutTimer: Timer? + /// the request timeout interval + private var requestTimeoutInterval: TimeInterval = IMAPlugin.defaultTimeoutInterval + /************************************************************/ // MARK: - IMAContentPlayhead /************************************************************/ @@ -76,8 +80,7 @@ extension PKAdInfo { public static func warmUp() { // load adsLoader in order to make IMA download the needed objects before initializing. // will setup the instance when first player is loaded - let imaSettings: IMASettings = IMASettings() - let imaLoader = IMAAdsLoader(settings: imaSettings) + _ = IMAAdsLoader(settings: IMASettings()) } /************************************************************/ @@ -88,12 +91,12 @@ extension PKAdInfo { public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) throws { try super.init(player: player, pluginConfig: pluginConfig, messageBus: messageBus) - if let adsConfig = pluginConfig as? AdsConfig { + if let adsConfig = pluginConfig as? IMAConfig { self.config = adsConfig + self.requestTimeoutInterval = adsConfig.requestTimeoutInterval if IMAPlugin.loader == nil { self.setupLoader(with: adsConfig) } - IMAPlugin.loader.contentComplete() IMAPlugin.loader.delegate = self } else { @@ -111,19 +114,20 @@ extension PKAdInfo { super.onUpdateConfig(pluginConfig: pluginConfig) - if let adsConfig = pluginConfig as? AdsConfig { + if let adsConfig = pluginConfig as? IMAConfig { self.config = adsConfig } } // TODO:: finilize update config & updateMedia logic public override func onUpdateMedia(mediaConfig: MediaConfig) { - PKLog.debug("mediaConfig: " + String(describing: mediaConfig)) super.onUpdateMedia(mediaConfig: mediaConfig) } public override func destroy() { super.destroy() + self.requestTimeoutTimer?.invalidate() + self.requestTimeoutTimer = nil self.destroyManager() } @@ -139,76 +143,223 @@ extension PKAdInfo { // MARK: - AdsPlugin /************************************************************/ + var isAdPlaying: Bool { + return self.stateMachine.getState() == .adsPlaying + } + func requestAds() { - guard let playerView = player?.view else { return } + guard let player = self.player else { return } - if self.config.adTagUrl != nil && self.config.adTagUrl != "" { - self.startAdCalled = false - - // setup ad display container and companion if exists, needs to create a new ad container for each request. - var companionAdSlot: IMACompanionAdSlot? = nil - let adDisplayContainer: IMAAdDisplayContainer - if let companionView = self.config?.companionView { - companionAdSlot = IMACompanionAdSlot(view: companionView, width: Int32(companionView.frame.size.width), height: Int32(companionView.frame.size.height)) - adDisplayContainer = IMAAdDisplayContainer(adContainer: playerView, companionSlots: [companionAdSlot!]) - } else { - adDisplayContainer = IMAAdDisplayContainer(adContainer: playerView, companionSlots: []) + let adDisplayContainer = self.createAdDisplayContainer(forView: player.view) + 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) + default: break // should not receive timeout for any other state + } + // set state to request failure + self.stateMachine.set(state: .adsRequestTimedOut) + + self.invalidateRequestTimer() + // post ads request timeout event + self.notify(event: AdEvent.RequestTimedOut()) } - - var request: IMAAdsRequest - request = IMAAdsRequest(adTagUrl: self.config.adTagUrl, adDisplayContainer: adDisplayContainer, contentPlayhead: self, userContext: nil) - - IMAPlugin.loader.requestAds(with: request) - PKLog.trace("request Ads") + } + PKLog.trace("request Ads") + } + + func resume() { + self.adsManager?.resume() + } + + func pause() { + self.adsManager?.pause() + } + + func contentComplete() { + IMAPlugin.loader.contentComplete() + } + + func destroyManager() { + self.adsManager?.delegate = nil + self.adsManager?.destroy() + // In order to make multiple ad requests, AdsManager instance should be destroyed, and then contentComplete() should be called on AdsLoader. + // This will "reset" the SDK. + self.contentComplete() + self.adsManager = nil + // reset the state machine + self.stateMachine.reset() + } + + // when play() was used set state to content playing + func didPlay() { + self.stateMachine.set(state: .contentPlaying) + } + + func didRequestPlay(ofType type: AdsEnabledPlayerController.PlayType) { + switch self.stateMachine.getState() { + case .adsLoaded: self.startAd() + case .adsRequested: self.stateMachine.set(state: .adsRequestedAndPlay) + case .adsPlaying: self.resume() + default: self.delegate?.play(type) } } - @discardableResult - func start(showLoadingView: Bool) -> Bool { - if self.loaderFailed { - return false + /************************************************************/ + // MARK: - AdsLoaderDelegate + /************************************************************/ + + public func adsLoader(_ loader: IMAAdsLoader!, adsLoadedWith adsLoadedData: IMAAdsLoadedData!) { + switch self.stateMachine.getState() { + case .adsRequested: self.stateMachine.set(state: .adsLoaded) + case .adsRequestedAndPlay: self.stateMachine.set(state: .adsLoadedAndPlay) + default: self.invalidateRequestTimer() } - if self.config.adTagUrl != nil && self.config.adTagUrl != "" { - if showLoadingView { - self.showLoadingView(true, alpha: 1) + self.adsManager = adsLoadedData.adsManager + adsLoadedData.adsManager.delegate = self + self.createRenderingSettings() + + // initialize on ads manager starts the ads loading process, we want to initialize it only after play. + // `adsLoaded` state is when ads request succeeded but play haven't been received yet, + // we don't want to initialize ads manager until play() will be used. + if self.stateMachine.getState() != .adsLoaded { + self.initAdsManager() + } + } + + public func adsLoader(_ loader: IMAAdsLoader!, failedWith adErrorData: IMAAdLoadingErrorData!) { + // cancel the request timer + self.invalidateRequestTimer() + self.stateMachine.set(state: .adsRequestFailed) + self.showLoadingView(false, alpha: 0) + PKLog.error(adErrorData.adError.message) + self.messageBus?.post(AdEvent.Error(nsError: IMAPluginError(adError: adErrorData.adError).asNSError)) + self.delegate?.adsPlugin(self, loaderFailedWith: adErrorData.adError.message) + } + + /************************************************************/ + // MARK: - AdsManagerDelegate + /************************************************************/ + + 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!) { + PKLog.trace("ads manager event: " + String(describing: event)) + let currentState = self.stateMachine.getState() + + switch event.type { + // Ad break, will be called before each scheduled ad break. Ad breaks may contain more than 1 ad. + // `event.ad` is not available at this point do not use it here. + case .AD_BREAK_READY: + if shouldDiscardAd() { + PKLog.debug("discard Ad Break") + } else { + self.notify(event: AdEvent.AdBreakReady()) + guard canPlayAd(forState: currentState) else { return } + self.start(adsManager: adsManager) } - - if let adsManager = self.adsManager { - adsManager.initialize(with: self.renderingSettings) - self.notifyAdCuePoints(fromAdsManager: adsManager) + // single ad only fires `LOADED` without `AD_BREAK_READY`. + case .LOADED: + if shouldDiscard(ad: event.ad, currentState: currentState) { + self.discardAdBreak(adsManager: adsManager) } else { - self.startAdCalled = true + let adEvent = event.ad != nil ? AdEvent.AdLoaded(adInfo: PKAdInfo(ad: event.ad)) : AdEvent.AdLoaded() + self.notify(event: adEvent) + // if we have more than one ad don't start the manager, it will be handled in `AD_BREAK_READY` + guard adsManager.adCuePoints.count == 0 else { return } + guard canPlayAd(forState: currentState) else { return } + self.start(adsManager: adsManager) } - return true + case .STARTED: + self.stateMachine.set(state: .adsPlaying) + let event = event.ad != nil ? AdEvent.AdStarted(adInfo: PKAdInfo(ad: event.ad)) : AdEvent.AdStarted() + self.notify(event: event) + self.showLoadingView(false, alpha: 0) + case .ALL_ADS_COMPLETED: + // detaching the delegate and destroying the adsManager. + // means all ads have been played so we can destroy the adsManager. + self.destroyManager() + self.notify(event: AdEvent.AllAdsCompleted()) + case .CLICKED: self.notify(event: AdEvent.AdClicked()) + case .COMPLETE: self.notify(event: AdEvent.AdComplete()) + 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 .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: break } - return false } - func resume() { - self.adsManager?.resume() + public func adsManager(_ adsManager: IMAAdsManager!, didReceive error: IMAAdError!) { + self.showLoadingView(false, alpha: 0) + PKLog.error(error.message) + self.messageBus?.post(AdEvent.Error(nsError: IMAPluginError(adError: error).asNSError)) + self.delegate?.adsPlugin(self, managerFailedWith: error.message) } - func pause() { - self.adsManager?.pause() + public func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager!) { + self.stateMachine.set(state: .adsPlaying) + self.notify(event: AdEvent.AdDidRequestContentPause()) } - func contentComplete() { - IMAPlugin.loader.contentComplete() + public func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager!) { + self.stateMachine.set(state: .contentPlaying) + self.showLoadingView(false, alpha: 0) + self.notify(event: AdEvent.AdDidRequestContentResume()) + } + + public func adsManager(_ adsManager: IMAAdsManager!, adDidProgressToTime mediaTime: TimeInterval, totalTime: TimeInterval) { + self.notify(event: AdEvent.AdDidProgressToTime(mediaTime: mediaTime, totalTime: totalTime)) } /************************************************************/ // MARK: - Private /************************************************************/ - private func setupLoader(with config: AdsConfig) { + private func setupLoader(with config: IMAConfig) { let imaSettings: IMASettings! = IMASettings() imaSettings.language = config.language imaSettings.enableBackgroundPlayback = config.enableBackgroundPlayback imaSettings.autoPlayAdBreaks = config.autoPlayAdBreaks + imaSettings.enableDebugMode = config.enableDebugMode IMAPlugin.loader = IMAAdsLoader(settings: imaSettings) } + private func createAdDisplayContainer(forView view: UIView) -> IMAAdDisplayContainer { + // setup ad display container and companion if exists, needs to create a new ad container for each request. + if let companionView = self.config?.companionView { + let companionAdSlot = IMACompanionAdSlot(view: companionView, width: Int32(companionView.frame.size.width), height: Int32(companionView.frame.size.height)) + return IMAAdDisplayContainer(adContainer: view, companionSlots: [companionAdSlot!]) + } else { + return IMAAdDisplayContainer(adContainer: view, companionSlots: []) + } + } + private func setupLoadingView() { self.loadingView = UIView(frame: CGRect.zero) self.loadingView!.translatesAutoresizingMaskIntoConstraints = false @@ -253,10 +404,10 @@ extension PKAdInfo { self.loadingView!.alpha = alpha self.loadingView!.isHidden = !show - + self.player?.view?.bringSubview(toFront: self.loadingView!) } - + private func notify(event: AdEvent) { self.delegate?.adsPlugin(self, didReceive: event) self.messageBus?.post(event) @@ -265,132 +416,64 @@ extension PKAdInfo { private func notifyAdCuePoints(fromAdsManager adsManager: IMAAdsManager) { // send ad cue points if exists and request is url type let adCuePoints = adsManager.getAdCuePoints() - if self.config.adTagUrl != nil && adCuePoints.count > 0 { + if adCuePoints.count > 0 { self.notify(event: AdEvent.AdCuePointsUpdate(adCuePoints: adCuePoints)) } } - func destroyManager() { - self.isAdPlayback = false - self.startAdCalled = false - self.loaderFailed = false - self.adsManager?.delegate = nil - self.adsManager?.destroy() - // In order to make multiple ad requests, AdsManager instance should be destroyed, and then contentComplete() should be called on AdsLoader. - // This will "reset" the SDK. - self.contentComplete() - self.adsManager = nil + private func start(adsManager: IMAAdsManager) { + if let canPlay = self.dataSource?.adsPluginShouldPlayAd(self), canPlay == true { + adsManager.start() + } } - - /************************************************************/ - // MARK: - AdsLoaderDelegate - /************************************************************/ - public func adsLoader(_ loader: IMAAdsLoader!, adsLoadedWith adsLoadedData: IMAAdsLoadedData!) { - self.loaderFailed = false - - self.adsManager = adsLoadedData.adsManager - adsLoadedData.adsManager.delegate = self - self.createRenderingSettings() - - if self.startAdCalled { - self.adsManager!.initialize(with: self.renderingSettings) - self.notifyAdCuePoints(fromAdsManager: self.adsManager!) - } + private func initAdsManager() { + self.adsManager!.initialize(with: self.renderingSettings) PKLog.debug("ads manager set") + self.notifyAdCuePoints(fromAdsManager: self.adsManager!) } - public func adsLoader(_ loader: IMAAdsLoader!, failedWith adErrorData: IMAAdLoadingErrorData!) { - self.loaderFailed = true - self.showLoadingView(false, alpha: 0) - PKLog.error(adErrorData.adError.message) - self.messageBus?.post(AdEvent.Error(nsError: IMAPluginError(adError: adErrorData.adError).asNSError)) - self.delegate?.adsPlugin(self, loaderFailedWith: adErrorData.adError.message) + private func invalidateRequestTimer() { + self.requestTimeoutTimer?.invalidate() + self.requestTimeoutTimer = nil } - /************************************************************/ - // MARK: - AdsManagerDelegate - /************************************************************/ - - public func adsManagerAdDidStartBuffering(_ adsManager: IMAAdsManager!) { - self.showLoadingView(true, alpha: 0.1) + /// called when plugin need to start the ad playback on first ad play only + private func startAd() { + self.stateMachine.set(state: .adsLoadedAndPlay) + self.initAdsManager() } - public func adsManagerAdPlaybackReady(_ adsManager: IMAAdsManager!) { - self.showLoadingView(false, alpha: 0) - } - - public func adsManager(_ adsManager: IMAAdsManager!, didReceive event: IMAAdEvent!) { - PKLog.debug("ads manager event: " + String(describing: event)) - switch event.type { - // Ad break, will be called before each scheduled ad break. Ad breaks may contain more than 1 ad. - case .AD_BREAK_READY: - self.notify(event: AdEvent.AdBreakReady()) - let canPlay = self.dataSource?.adsPluginShouldPlayAd(self) - if canPlay == nil || canPlay == true { - adsManager.start() - } - case .LOADED: - self.notify(event: AdEvent.AdLoaded()) - // single ad only fires `LOADED` without `AD_BREAK_READY`. - // if we have more than one ad don't handle the event, it will be handled in `AD_BREAK_READY` - if adsManager.adCuePoints.count == 0 { - let canPlay = self.dataSource?.adsPluginShouldPlayAd(self) - if canPlay == nil || canPlay == true { - adsManager.start() - } else { - adsManager.skip() - self.adsManagerDidRequestContentResume(adsManager) - } - } - case .STARTED: - 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. - self.destroyManager() - 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()) + /// protects against cases where the ads manager will load after timeout. + /// this way we will only start ads when ads loaded and play() was used or when we came from content playing. + private func canPlayAd(forState state: IMAState) -> Bool { + if state == .adsLoadedAndPlay || state == .contentPlaying { + return true } + return false } - public func adsManager(_ adsManager: IMAAdsManager!, didReceive error: IMAAdError!) { - self.showLoadingView(false, alpha: 0) - PKLog.error(error.message) - self.messageBus?.post(AdEvent.Error(nsError: IMAPluginError(adError: error).asNSError)) - self.delegate?.adsPlugin(self, managerFailedWith: error.message) - } - - public func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager!) { - self.isAdPlayback = true - self.notify(event: AdEvent.AdDidRequestPause()) + private func shouldDiscardAd() -> Bool { + if self.currentTime < self.dataSource?.adsPluginStartTime ?? 0 { + return true + } + return false } - public func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager!) { - self.isAdPlayback = false - self.showLoadingView(false, alpha: 0) - self.notify(event: AdEvent.AdDidRequestResume()) + private func shouldDiscard(ad: IMAAd, currentState: IMAState) -> Bool { + let adInfo = PKAdInfo(ad: ad) + let isStartTimeInvalid = adInfo.positionType != .postRoll && adInfo.timeOffset < self.dataSource?.adsPluginStartTime ?? 0 + let isPreRollInvalid = adInfo.positionType == .preRoll && (currentState == .adsRequestTimedOut || currentState == .contentPlaying) + if isStartTimeInvalid || isPreRollInvalid { + return true + } + return false } - public func adsManager(_ adsManager: IMAAdsManager!, adDidProgressToTime mediaTime: TimeInterval, totalTime: TimeInterval) { - self.notify(event: AdEvent.AdDidProgressToTime(mediaTime: mediaTime, totalTime: totalTime)) + private func discardAdBreak(adsManager: IMAAdsManager) { + PKLog.debug("discard Ad Break") + adsManager.discardAdBreak() + self.adsManagerDidRequestContentResume(adsManager) } /************************************************************/ diff --git a/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift b/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift index 8ce6edb7..e6a0b183 100644 --- a/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift +++ b/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift @@ -5,6 +5,7 @@ // Created by Oded Klein on 01/12/2016. // // +import KalturaNetKit /// `KalturaStatsEvent` represents an event reporting from kaltura stats plugin. @objc public class KalturaLiveStatsEvent: PKEvent { @@ -84,7 +85,7 @@ public class KalturaLiveStatsPlugin: BaseAnalyticsPlugin { override var playerEventsToRegister: [PlayerEvent.Type] { return [ PlayerEvent.play, - PlayerEvent.playbackParamsUpdated, + PlayerEvent.playbackInfo, PlayerEvent.pause, PlayerEvent.stateChanged ] @@ -110,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: @@ -204,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/KalturaLiveStats/LiveStatsService.swift b/Plugins/KalturaLiveStats/LiveStatsService.swift index ad5ee24b..9e44624a 100644 --- a/Plugins/KalturaLiveStats/LiveStatsService.swift +++ b/Plugins/KalturaLiveStats/LiveStatsService.swift @@ -8,6 +8,7 @@ import UIKit import SwiftyJSON +import KalturaNetKit internal class LiveStatsService { diff --git a/Plugins/KalturaStats/KalturaStatsPlugin.swift b/Plugins/KalturaStats/KalturaStatsPlugin.swift index 350d747b..80b2bb5c 100644 --- a/Plugins/KalturaStats/KalturaStatsPlugin.swift +++ b/Plugins/KalturaStats/KalturaStatsPlugin.swift @@ -6,6 +6,8 @@ // // +import KalturaNetKit + /// `KalturaStatsEvent` represents an event reporting from kaltura stats plugin. @objc public class KalturaStatsEvent: PKEvent { @@ -297,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 = "" diff --git a/Plugins/KalturaStats/OVPStatsService.swift b/Plugins/KalturaStats/OVPStatsService.swift index 43b3525d..69e84893 100644 --- a/Plugins/KalturaStats/OVPStatsService.swift +++ b/Plugins/KalturaStats/OVPStatsService.swift @@ -8,6 +8,7 @@ import UIKit import SwiftyJSON +import KalturaNetKit internal class OVPStatsService { diff --git a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift index e825bc66..0a8a4255 100644 --- a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift +++ b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift @@ -7,6 +7,7 @@ // import Foundation +import KalturaNetKit /// class `BaseOTTAnalyticsPlugin` is a base plugin object used for OTT analytics plugin subclasses public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProtocol, AppStateObservable { @@ -14,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 @@ -27,12 +29,16 @@ 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() - self.sendAnalyticsEvent(ofType: .stop) + // only send stop event if content started playing already & content is not ended + if !self.isFirstPlay && !self.isContentEnded { + self.sendAnalyticsEvent(ofType: .stop) + } self.timer?.invalidate() AppStateSubject.shared.remove(observer: self) } @@ -61,7 +67,8 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt PlayerEvent.error, PlayerEvent.pause, PlayerEvent.loadedMetadata, - PlayerEvent.playing + PlayerEvent.playing, + PlayerEvent.seeked ] } @@ -72,12 +79,14 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt PKLog.debug("Register event: \(event.self)") switch event { + 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 } 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 @@ -115,7 +124,7 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt if strongSelf.isFirstPlay { strongSelf.isFirstPlay = false - strongSelf.sendAnalyticsEvent(ofType: .firstPlay); + strongSelf.sendAnalyticsEvent(ofType: .first_play); } else { strongSelf.sendAnalyticsEvent(ofType: .play); } diff --git a/Plugins/Phoenix/BookmarkService.swift b/Plugins/Phoenix/BookmarkService.swift index 70ce05ab..2e3a0471 100644 --- a/Plugins/Phoenix/BookmarkService.swift +++ b/Plugins/Phoenix/BookmarkService.swift @@ -8,6 +8,7 @@ import UIKit import SwiftyJSON +import KalturaNetKit internal class BookmarkService { diff --git a/Plugins/Phoenix/MediaMarkService.swift b/Plugins/Phoenix/MediaMarkService.swift index 5b20f9b0..06f3073b 100644 --- a/Plugins/Phoenix/MediaMarkService.swift +++ b/Plugins/Phoenix/MediaMarkService.swift @@ -8,6 +8,7 @@ import UIKit import SwiftyJSON +import KalturaNetKit internal class MediaMarkService { diff --git a/Plugins/Phoenix/OTTAnalyticsPluginProtocol.swift b/Plugins/Phoenix/OTTAnalyticsPluginProtocol.swift index b81c642a..0b1e8030 100644 --- a/Plugins/Phoenix/OTTAnalyticsPluginProtocol.swift +++ b/Plugins/Phoenix/OTTAnalyticsPluginProtocol.swift @@ -7,13 +7,14 @@ // import Foundation +import KalturaNetKit enum OTTAnalyticsEventType: String { case hit case play case stop case pause - case firstPlay + case first_play case swoosh case load case finish diff --git a/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift b/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift index f968ad46..f80e6480 100644 --- a/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift +++ b/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift @@ -7,6 +7,7 @@ // import UIKit +import KalturaNetKit public class PhoenixAnalyticsPlugin: BaseOTTAnalyticsPlugin { @@ -62,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 d8f72ac4..629a6baf 100644 --- a/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift +++ b/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift @@ -8,6 +8,7 @@ import UIKit import SwiftyJSON +import KalturaNetKit public class TVPAPIAnalyticsPlugin: BaseOTTAnalyticsPlugin { @@ -58,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() } 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 +}