From ab2972f7eb6aaa1dcd57f1e913fa760041b07226 Mon Sep 17 00:00:00 2001 From: Oren Me Date: Thu, 23 Mar 2017 14:23:25 +0200 Subject: [PATCH 01/27] Remove BETA note --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b70526a..ba08c1b8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Platform](https://img.shields.io/cocoapods/p/PlayKit.svg?style=flat)](https://cocoapods.org/pods/PlayKit) # Kaltura Player SDK -## Note: The Kaltura SDK v3 is in beta + ### Demo: [Demo repo](https://github.com/kaltura/playkit-ios-samples). *If you are a Kaltura customer, please contact your Kaltura Customer Success Manager to help facilitate use of this component.* From 1451fba97f63c440954072307c7dfd1d5c1c1cfa Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Sun, 26 Mar 2017 11:31:23 +0300 Subject: [PATCH 02/27] Fix issue on iOS 8,9 where rate stays 1.0 even after played to end time. (#118) Now when play to end time event received calls pause to make sure rate will be 0.0. --- .../Player/AVPlayerEngine/AVPlayerEngine+Observation.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift index e8bf5da3..b3b9bae3 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift @@ -96,6 +96,11 @@ extension AVPlayerEngine { self.postStateChange(newState: newState, oldState: self.currentState) self.currentState = newState self.isPlayedToEndTime = true + // In iOS 9 and below rate is 1.0 even when playback is finished. + // To make sure rate will be 0.0 (paused) when played to end we call pause manually. + // calling pause after `isPlayedToEndTime` will make sure no pause event will be sent in messageBus. + self.pause() + // pause should be called before ended to make sure our rate will be 0.0 when ended event will be observed. self.post(event: PlayerEvent.Ended()) } From 47da8eca020c9a95faa6fff7cba368794da94986 Mon Sep 17 00:00:00 2001 From: srivkas Date: Mon, 27 Mar 2017 14:09:54 +0300 Subject: [PATCH 03/27] Fem 1165: Pheonix GetPlaybackContext (#112) * Renaming ott media provider to ovp media provider * Update tests to the latest code * Adding first implementation for Phoenix media provider with playback context * fixing compilation issue * Fixing the test and changing the tested asset * Fixing object type issue * deleting unnecessary enum * Adding condition for not supported format and schemes * Adding udid to OTT session * Adding social service ( in order to login via Facebook ) * adding did to login and refresh api's * Adding recovery session method adding udid adding '{}' to forward parameter * Error handling * set recovery params as optional set token expiration getter public * Adding logout and creating class for session info * Fixing asset builder DRMSupport checker * fixing Facebook login request builder * removing logout from session when refresh is failed * Adding documentation for phoenix media provider * delete unnecessary errors * Adding switch profile API unifying all session requests into one * changing safety margin to be mutable * Fixing codacy issues * Fixing codacy issues * swiftlint Classes/Providers/OTT. * More swiftlint. --- Classes/PKError.swift | 2 +- Classes/Player/AssetBuilder.swift | 4 +- Classes/Player/MediaEntry.swift | 5 +- Classes/Providers/Base/FormatsHelper.swift | 32 ++ Classes/Providers/MediaEntryProvider.swift | 10 +- .../Providers/Mock/MockMediaProvider.swift | 36 +- Classes/Providers/OTT/Model/OTTAsset.swift | 15 +- .../Providers/OTT/Model/OTTBaseObject.swift | 4 +- Classes/Providers/OTT/Model/OTTDrmData.swift | 31 ++ Classes/Providers/OTT/Model/OTTError.swift | 22 +- Classes/Providers/OTT/Model/OTTFile.swift | 24 +- .../OTT/Model/OTTGetAssetResponse.swift | 9 +- .../Providers/OTT/Model/OTTLicensedURL.swift | 11 +- .../OTT/Model/OTTLoginResponse.swift | 10 +- .../Providers/OTT/Model/OTTLoginSession.swift | 8 +- .../OTT/Model/OTTPlaybackContext.swift | 24 + .../OTT/Model/OTTPlaybackSource.swift | 58 +++ .../OTT/Model/OTTRefreshedSession.swift | 10 +- Classes/Providers/OTT/Model/OTTSession.swift | 11 +- Classes/Providers/OTT/OTTMediaProvider.swift | 156 ------ .../OTT/Parsers/OTTMultiResponseParser.swift | 18 +- .../OTT/Parsers/OTTObjectMapper.swift | 12 +- .../OTT/Parsers/OTTResponseParser.swift | 11 +- .../Providers/OTT/PhoenixMediaProvider.swift | 395 +++++++++++++++ .../OTT/Services/OTTAssetService.swift | 52 +- .../OTT/Services/OTTLicensedURLService.swift | 9 +- .../OTT/Services/OTTSessionService.swift | 22 +- .../OTT/Services/OTTSocialService.swift | 41 ++ .../OTT/Services/OTTUserService.swift | 39 +- .../OTT/Services/PhoenixAPIDefines.swift | 22 + .../OTT/Session/OTTSessionManager.swift | 464 ++++++++++++------ Classes/Providers/OVP/Model/OVPSource.swift | 10 +- Classes/Providers/OVP/OVPMediaProvider.swift | 27 +- Example/PlayKit.xcodeproj/project.pbxproj | 16 +- Example/Podfile.lock | 2 +- .../MockMediaProviderTest.swift | 17 +- .../OTTMediaProviderTest.swift | 60 --- .../OTTSessionProviderTest.swift | 7 +- .../OVPMediaProviederTest.swift | 15 +- .../PhoenixMediaProviderTest.swift | 58 +++ Example/Tests/MessegeBusTest.swift | 4 +- Example/Tests/PlayerControllerTest.swift | 6 +- Example/Tests/PlayerCreator.swift | 22 +- 43 files changed, 1228 insertions(+), 583 deletions(-) create mode 100644 Classes/Providers/Base/FormatsHelper.swift create mode 100644 Classes/Providers/OTT/Model/OTTDrmData.swift create mode 100644 Classes/Providers/OTT/Model/OTTPlaybackContext.swift create mode 100644 Classes/Providers/OTT/Model/OTTPlaybackSource.swift delete mode 100644 Classes/Providers/OTT/OTTMediaProvider.swift create mode 100644 Classes/Providers/OTT/PhoenixMediaProvider.swift create mode 100644 Classes/Providers/OTT/Services/OTTSocialService.swift create mode 100644 Classes/Providers/OTT/Services/PhoenixAPIDefines.swift delete mode 100644 Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift create mode 100644 Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift diff --git a/Classes/PKError.swift b/Classes/PKError.swift index ebcb963f..3e3cb0e5 100644 --- a/Classes/PKError.swift +++ b/Classes/PKError.swift @@ -160,7 +160,7 @@ protocol PKError: Error, CustomStringConvertible { extension PKError { /// description string - var description: String { + public var description: String { return "\(type(of: self)) ,domain: \(type(of: self).domain), errorCode: \(self.code)" } diff --git a/Classes/Player/AssetBuilder.swift b/Classes/Player/AssetBuilder.swift index c8740a6e..928974d9 100644 --- a/Classes/Player/AssetBuilder.swift +++ b/Classes/Player/AssetBuilder.swift @@ -99,7 +99,7 @@ enum AssetError : Error { class DRMSupport { // FairPlay is not available in simulators and before iOS8 static let fairplay: Bool = { - if Platform.isSimulator, #available(iOS 8, *) { + if !Platform.isSimulator, #available(iOS 8, *) { return true } else { return false @@ -108,7 +108,7 @@ class DRMSupport { // FairPlay is not available in simulators and is only downloadable in iOS10 and up. static let fairplayOffline: Bool = { - if Platform.isSimulator, #available(iOS 10, *) { + if !Platform.isSimulator, #available(iOS 10, *) { return true } else { return false diff --git a/Classes/Player/MediaEntry.swift b/Classes/Player/MediaEntry.swift index 86b8b30b..958fb477 100644 --- a/Classes/Player/MediaEntry.swift +++ b/Classes/Player/MediaEntry.swift @@ -15,6 +15,7 @@ func getJson(_ json: Any) -> JSON { @objc public enum MediaType: Int { case live + case vod case unknown } @@ -136,7 +137,6 @@ func getJson(_ json: Any) -> JSON { private let idKey: String = "id" private let contentUrlKey: String = "url" - private let mimeTypeKey: String = "mimeType" private let drmDataKey: String = "drmData" private let formatTypeKey: String = "sourceType" @@ -147,7 +147,6 @@ func getJson(_ json: Any) -> JSON { @objc public init(_ id: String, contentUrl: URL?, mimeType: String? = nil, drmData: [DRMParams]? = nil, mediaFormat: MediaFormat = .unknown) { self.id = id self.contentUrl = contentUrl - self.mimeType = mimeType self.drmData = drmData self.mediaFormat = mediaFormat } @@ -160,8 +159,6 @@ func getJson(_ json: Any) -> JSON { self.contentUrl = sj[contentUrlKey].url - self.mimeType = sj[mimeTypeKey].string - if let drmData = sj[drmDataKey].array { self.drmData = drmData.flatMap { DRMParams.fromJSON($0) } } diff --git a/Classes/Providers/Base/FormatsHelper.swift b/Classes/Providers/Base/FormatsHelper.swift new file mode 100644 index 00000000..d0e2cb5d --- /dev/null +++ b/Classes/Providers/Base/FormatsHelper.swift @@ -0,0 +1,32 @@ +// +// FormatsHelper.swift +// Pods +// +// Created by Rivka Peleg on 05/03/2017. +// +// + +import Foundation + +public class FormatsHelper { + + static let supportedFormats: [MediaSource.MediaFormat] = [.hls, .mp4, .wvm, .mp3] + static let supportedSchemes: [DRMParams.Scheme] = [.fairplay, .widevineClassic] + + static func getMediaFormat (format: String, hasDrm: Bool) -> MediaSource.MediaFormat { + + switch format { + case "applehttp": + return .hls + case "url": + if hasDrm { + return .wvm + } else { + return .mp4 + } + default: + return .unknown + } + } + +} diff --git a/Classes/Providers/MediaEntryProvider.swift b/Classes/Providers/MediaEntryProvider.swift index 48fcb97d..fe20b569 100644 --- a/Classes/Providers/MediaEntryProvider.swift +++ b/Classes/Providers/MediaEntryProvider.swift @@ -8,7 +8,6 @@ import UIKit - @objc public protocol MediaEntryProvider { /** This method is triggering the creation of media base on custom parameters and actions. @@ -32,12 +31,7 @@ import UIKit */ func loadMedia(callback: @escaping (MediaEntry?, Error?) -> Void) - - - func cancel() - -} - - + func cancel() +} diff --git a/Classes/Providers/Mock/MockMediaProvider.swift b/Classes/Providers/Mock/MockMediaProvider.swift index ebf40b7f..4a133e3c 100644 --- a/Classes/Providers/Mock/MockMediaProvider.swift +++ b/Classes/Providers/Mock/MockMediaProvider.swift @@ -10,52 +10,52 @@ import UIKit import SwiftyJSON @objc public class MockMediaEntryProvider: NSObject, MediaEntryProvider { - + public enum MockError: Error { case invalidParam(paramName:String) case fileIsEmptyOrNotFound case unableToParseJSON case mediaNotFound } - + @objc public var id: String? @objc public var url: URL? @objc public var content: Any? - + @discardableResult @nonobjc public func set(id: String?) -> Self { self.id = id return self } - + @discardableResult @nonobjc public func set(url: URL?) -> Self { self.url = url return self } - + @discardableResult @nonobjc public func set(content: Any?) -> Self { self.content = content return self } - - public override init(){ - + + public override init() { + } - + struct LoaderInfo { var id: String var content: JSON } @objc public func loadMedia(callback: @escaping (MediaEntry?, Error?) -> Void) { - + guard let id = self.id else { callback(nil, MockError.invalidParam(paramName: "id")) return } - + var json: JSON? = nil if let content = self.content { json = JSON(content) @@ -70,30 +70,30 @@ import SwiftyJSON } json = JSON(data: data as Data) } - + guard let jsonContent = json else { callback(nil, MockError.unableToParseJSON) return } - + let loderInfo = LoaderInfo(id: id, content: jsonContent) - + guard loderInfo.content != .null else { callback(nil, MockError.unableToParseJSON) return } - + let jsonObject: JSON = loderInfo.content[loderInfo.id] guard jsonObject != .null else { callback(nil, MockError.mediaNotFound) return } - + let mediaEntry = MediaEntry(json: jsonObject.object) callback(mediaEntry, nil) } - + public func cancel() { - + } } diff --git a/Classes/Providers/OTT/Model/OTTAsset.swift b/Classes/Providers/OTT/Model/OTTAsset.swift index af065b38..9dbbe7b3 100644 --- a/Classes/Providers/OTT/Model/OTTAsset.swift +++ b/Classes/Providers/OTT/Model/OTTAsset.swift @@ -11,29 +11,28 @@ import SwiftyJSON internal class OTTAsset: OTTBaseObject { - internal var id: String + internal var id: String internal var files: [OTTFile]? - + private let idKey = "id" private let idfiles = "mediaFiles" - + internal 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){ + if let file = OTTFile(json: jsonFile.object) { self.files?.append(file) } } } } } - diff --git a/Classes/Providers/OTT/Model/OTTBaseObject.swift b/Classes/Providers/OTT/Model/OTTBaseObject.swift index 654a285c..03bc15ac 100644 --- a/Classes/Providers/OTT/Model/OTTBaseObject.swift +++ b/Classes/Providers/OTT/Model/OTTBaseObject.swift @@ -9,8 +9,6 @@ import UIKit protocol OTTBaseObject { - + init?(json:Any) } - - diff --git a/Classes/Providers/OTT/Model/OTTDrmData.swift b/Classes/Providers/OTT/Model/OTTDrmData.swift new file mode 100644 index 00000000..9ef4d02a --- /dev/null +++ b/Classes/Providers/OTT/Model/OTTDrmData.swift @@ -0,0 +1,31 @@ +// +// OTTDrmPlaybackPluginData.swift +// Pods +// +// Created by Rivka Peleg on 05/03/2017. +// +// + +import Foundation +import SwiftyJSON + +class OTTDrmData: OTTBaseObject { + + var scheme: String + var licenseURL: String + var certificate: String? + + required init?(json: Any) { + let jsonObject = JSON(json) + + guard let scheme = jsonObject["scheme"].string, + let licenseURL = jsonObject["licenseURL"].string + else { + return nil + } + + self.scheme = scheme + self.licenseURL = licenseURL + self.certificate = jsonObject["certificate"].string + } +} diff --git a/Classes/Providers/OTT/Model/OTTError.swift b/Classes/Providers/OTT/Model/OTTError.swift index 6727f496..25905394 100644 --- a/Classes/Providers/OTT/Model/OTTError.swift +++ b/Classes/Providers/OTT/Model/OTTError.swift @@ -9,27 +9,21 @@ import UIKit import SwiftyJSON - - class OTTError: OTTBaseObject { - + var message: String? var code: String? - + let errorKey = "error" let messageKey = "message" let codeKey = "code" - - + required init?(json: Any) { - + let jsonObj: JSON = JSON(json) - self.message = jsonObj[errorKey][messageKey].string - self.code = jsonObj[errorKey][codeKey].string + let errorDict = jsonObj[errorKey] + self.message = errorDict[messageKey].string + self.code = errorDict[codeKey].string } - - init() { - - } - + } diff --git a/Classes/Providers/OTT/Model/OTTFile.swift b/Classes/Providers/OTT/Model/OTTFile.swift index 9ea06496..698836e7 100644 --- a/Classes/Providers/OTT/Model/OTTFile.swift +++ b/Classes/Providers/OTT/Model/OTTFile.swift @@ -10,33 +10,33 @@ import UIKit import SwiftyJSON internal class OTTFile: OTTBaseObject { - + internal var id: String - internal var type: String? = nil - internal var url: URL? = nil - internal var duration: TimeInterval? = nil - + 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){ + 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{ + } else { return nil } - + self.type = fileJosn[typeKey].string - if let contentURL = fileJosn[urlKey].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 index 7924d64d..36f8ee6d 100644 --- a/Classes/Providers/OTT/Model/OTTGetAssetResponse.swift +++ b/Classes/Providers/OTT/Model/OTTGetAssetResponse.swift @@ -11,15 +11,14 @@ import SwiftyJSON internal class OTTGetAssetResponse: OTTBaseObject { - internal var asset: OTTAsset? = nil - + 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 index 79f03709..ea40ec67 100644 --- a/Classes/Providers/OTT/Model/OTTLicensedURL.swift +++ b/Classes/Providers/OTT/Model/OTTLicensedURL.swift @@ -12,19 +12,18 @@ 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 index 0c3a1c10..263147e1 100644 --- a/Classes/Providers/OTT/Model/OTTLoginResponse.swift +++ b/Classes/Providers/OTT/Model/OTTLoginResponse.swift @@ -10,16 +10,16 @@ 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 index c1e652ef..4dc676b0 100644 --- a/Classes/Providers/OTT/Model/OTTLoginSession.swift +++ b/Classes/Providers/OTT/Model/OTTLoginSession.swift @@ -13,15 +13,15 @@ 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/OTTPlaybackContext.swift b/Classes/Providers/OTT/Model/OTTPlaybackContext.swift new file mode 100644 index 00000000..530e4557 --- /dev/null +++ b/Classes/Providers/OTT/Model/OTTPlaybackContext.swift @@ -0,0 +1,24 @@ +// +// OTTPlaybackContext.swift +// Pods +// +// Created by Rivka Peleg on 05/03/2017. +// +// + +import Foundation +import SwiftyJSON + +class OTTPlaybackContext: OTTBaseObject { + + var sources: [OTTPlaybackSource] = [] + + required init?(json: Any) { + let jsonObject = JSON(json) + jsonObject["sources"].array?.forEach { (source: JSON) in + if let source = OTTPlaybackSource(json: source.object) { + sources.append(source) + } + } + } +} diff --git a/Classes/Providers/OTT/Model/OTTPlaybackSource.swift b/Classes/Providers/OTT/Model/OTTPlaybackSource.swift new file mode 100644 index 00000000..e7f353f0 --- /dev/null +++ b/Classes/Providers/OTT/Model/OTTPlaybackSource.swift @@ -0,0 +1,58 @@ +// +// OTTPlaybackSource.swift +// Pods +// +// Created by Rivka Peleg on 05/03/2017. +// +// + +import Foundation +import SwiftyJSON + +class OTTPlaybackSource: OTTBaseObject { + + var assetId: Int + var id: Int + var type: String // file format + var url: URL? + var duration: Float + var externalId: String? + var protocols: [String] + var format: String + var drm: [OTTDrmData]? + + required init?(json: Any) { + let jsonObject = JSON(json) + + guard let assetId = jsonObject["assetId"].int, + let id = jsonObject["id"].int, + let type = jsonObject["type"].string, + let urlString = jsonObject["url"].string, + let protocolsString = jsonObject["protocols"].string, + let format = jsonObject["format"].string + else { + return nil + } + + self.assetId = assetId + self.id = id + self.type = type + self.url = URL.init(string: urlString) + self.protocols = protocolsString.components(separatedBy: ",") + self.format = format + self.duration = jsonObject["duration"].float ?? 0 + self.externalId = jsonObject["externalId"].string + + var drmArray = [OTTDrmData]() + jsonObject["drm"].array?.forEach {(json) in + if let drmObject = OTTDrmData(json: json.object) { + drmArray.append(drmObject) + } + } + + if drmArray.count > 0 { + self.drm = drmArray + } + + } +} diff --git a/Classes/Providers/OTT/Model/OTTRefreshedSession.swift b/Classes/Providers/OTT/Model/OTTRefreshedSession.swift index e61fb46b..eb143544 100644 --- a/Classes/Providers/OTT/Model/OTTRefreshedSession.swift +++ b/Classes/Providers/OTT/Model/OTTRefreshedSession.swift @@ -13,17 +13,15 @@ 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 index f6dd9716..60240116 100644 --- a/Classes/Providers/OTT/Model/OTTSession.swift +++ b/Classes/Providers/OTT/Model/OTTSession.swift @@ -9,18 +9,21 @@ 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{ + if let time = jsonObject[tokenExpirationKey].number?.doubleValue { self.tokenExpiration = Date.init(timeIntervalSince1970:time) } + self.udid = jsonObject[udidKey].string + } } diff --git a/Classes/Providers/OTT/OTTMediaProvider.swift b/Classes/Providers/OTT/OTTMediaProvider.swift deleted file mode 100644 index 70ec8f13..00000000 --- a/Classes/Providers/OTT/OTTMediaProvider.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// OTTEntryProvider.swift -// Pods -// -// Created by Admin on 13/11/2016. -// -// - -import UIKit -import SwiftyJSON - -@objc public class OTTMediaProvider: NSObject, MediaEntryProvider { - - public enum OTTMediaProviderError: Error { - case invalidInputParams - case invalidKS - case fileIsEmptyOrNotFound - case invalidJSON - case mediaNotFound - case currentlyProcessingOtherRequest - case unableToParseObject - } - - @objc public var sessionProvider: SessionProvider? - @objc public var mediaId: String? - @objc public var type: AssetType = .unknown - @objc public var formats: [String]? - public var executor: RequestExecutor? // TODO: make @objc if needed in the future - - public override init() {} - - @objc public init(_ sessionProvider: SessionProvider) { - self.sessionProvider = sessionProvider - } - - @discardableResult - @nonobjc public func set(sessionProvider: SessionProvider?) -> Self { - self.sessionProvider = sessionProvider - return self - } - - @discardableResult - @nonobjc public func set(mediaId:String?) -> Self { - self.mediaId = mediaId - return self - } - - @discardableResult - @nonobjc public func set(type: AssetType) -> Self { - self.type = type - return self - } - - @discardableResult - @nonobjc public func set(formats:[String]?) -> Self { - self.formats = formats - return self - } - - @discardableResult - @nonobjc public func set(executor:RequestExecutor?) -> Self { - self.executor = executor - return self - } - - struct LoaderInfo { - var sessionProvider: SessionProvider - var mediaId: String - var type: AssetType - var formats: [String] - var executor: RequestExecutor - } - - @objc public func loadMedia(callback: @escaping (MediaEntry?, Error?) -> Void) { - guard let sessionProvider = self.sessionProvider, - let mediaId = self.mediaId, - self.type != .unknown - else { - callback(nil, OTTMediaProviderError.invalidInputParams) - return - } - - var executor: RequestExecutor = USRExecutor.shared - var formats: [String] = [] - if let exe = self.executor{ - executor = exe - } - - if let fmts = self.formats { - formats = fmts - } - - let loaderParams = LoaderInfo(sessionProvider: sessionProvider, mediaId: mediaId, type: type, formats: formats, executor: executor) - self.startLoad(loader: loaderParams, callback: callback) - } - - public func cancel() { - - } - - func startLoad(loader: LoaderInfo, callback: @escaping (MediaEntry?, Error?) -> Void) { - loader.sessionProvider.loadKS { (ks, error) in - guard let ks = ks else { - callback(nil, OTTMediaProviderError.invalidKS) - return - } - - let requestBuilder = OTTAssetService.get(baseURL: loader.sessionProvider.serverURL, ks: ks, assetId: loader.mediaId, type:loader.type)? - .setOTTBasicParams() - .set(completion: { (r:Response) in - - guard let data = r.data else { - callback(nil, OTTMediaProviderError.mediaNotFound) - return - } - - var object: OTTBaseObject? = nil - do { - object = try OTTResponseParser.parse(data: data) - } catch { - callback(nil, error) - } - - if let asset = object as? OTTAsset { - - let mediaEntry: MediaEntry = MediaEntry(id: asset.id) - if let files = asset.files { - - var sources = [MediaSource]() - for file in files { - if let fileFormat = file.type{ - if loader.formats.contains(fileFormat) == true { - let source: MediaSource = MediaSource(id: file.id) - source.contentUrl = file.url - sources.append(source) - - } - } - } - - if sources.count > 0 { - mediaEntry.sources = sources - } - } - callback(mediaEntry, nil) - } else { - callback(nil, OTTMediaProviderError.mediaNotFound) - } - }) - if let assetRequest = requestBuilder?.build() { - loader.executor.send(request: assetRequest) - } - } - } -} - diff --git a/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift b/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift index f5195ea0..88822a76 100644 --- a/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift +++ b/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift @@ -10,33 +10,33 @@ import UIKit import SwiftyJSON class OTTMultiResponseParser: NSObject { - + enum OTTMultiResponseParserError: Error { case typeNotFound case emptyResponse case notMultiResponse } - + static func parse(data:Any) throws -> [OTTBaseObject] { - + let jsonResponse = JSON(data) if let resultArrayJSON = jsonResponse["result"].array { - + var resultArray: [OTTBaseObject] = [OTTBaseObject]() - for jsonObject: JSON in resultArrayJSON{ + for jsonObject: JSON in resultArrayJSON { var object: OTTBaseObject? = nil let objectType: OTTBaseObject.Type? = OTTObjectMapper.classByJsonObject(json: jsonObject.dictionaryObject) - if let type = objectType{ + if let type = objectType { object = type.init(json: jsonObject.object) } else { throw OTTMultiResponseParserError.typeNotFound } - - if let obj = object{ + + if let obj = object { resultArray.append(obj) } } - + return resultArray } else { return [OTTBaseObject]() diff --git a/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift b/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift index 0c81250e..eb5250ab 100644 --- a/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift +++ b/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift @@ -9,18 +9,16 @@ import UIKit import SwiftyJSON - - class OTTObjectMapper: NSObject { static let classNameKey = "objectType" - static let errorKey = "objectType" - + static let errorKey = "error" + static func classByJsonObject(json: Any?) -> OTTBaseObject.Type? { guard let js = json else { return nil } let jsonObject = JSON(js) let className = jsonObject[classNameKey].string - + if let name = className { switch name { case "KalturaLoginResponse": @@ -31,6 +29,10 @@ class OTTObjectMapper: NSObject { return OTTAsset.self case "KalturaLoginSession": return OTTLoginSession.self + case "KalturaPlaybackSource": + return OTTPlaybackSource.self + case "KalturaPlaybackContext": + return OTTPlaybackContext.self default: return nil } diff --git a/Classes/Providers/OTT/Parsers/OTTResponseParser.swift b/Classes/Providers/OTT/Parsers/OTTResponseParser.swift index eb6bcadf..f771ee7d 100644 --- a/Classes/Providers/OTT/Parsers/OTTResponseParser.swift +++ b/Classes/Providers/OTT/Parsers/OTTResponseParser.swift @@ -10,14 +10,14 @@ import UIKit import SwiftyJSON class OTTResponseParser: ResponseParser { - + enum OTTResponseParserError: Error { case typeNotFound case invalidJsonObject } - + static func parse(data:Any) throws -> OTTBaseObject { - + let jsonResponse = JSON(data) let resultObjectJSON = jsonResponse["result"].dictionaryObject let objectType: OTTBaseObject.Type? = OTTObjectMapper.classByJsonObject(json: resultObjectJSON) @@ -27,11 +27,8 @@ class OTTResponseParser: ResponseParser { } else { throw OTTResponseParserError.invalidJsonObject } - }else{ + } else { throw OTTResponseParserError.typeNotFound } } } - - - diff --git a/Classes/Providers/OTT/PhoenixMediaProvider.swift b/Classes/Providers/OTT/PhoenixMediaProvider.swift new file mode 100644 index 00000000..dc025273 --- /dev/null +++ b/Classes/Providers/OTT/PhoenixMediaProvider.swift @@ -0,0 +1,395 @@ +// +// OTTEntryProvider.swift +// +// +// Created by Admin on 13/11/2016. +// +// + +import UIKit +import SwiftyJSON + +/************************************************************/ +// MARK: - PhoenixMediaProviderError +/************************************************************/ +public enum PhoenixMediaProviderError: PKError { + + case invalidInputParam(param: String) + case unableToParseData(data: Any) + case noSourcesFound + case serverError(info:String) + + static let domain = "com.kaltura.playkit.error.PhoenixMediaProvider" + + var code: Int { + switch self { + case .invalidInputParam: return 0 + case .unableToParseData: return 1 + case .noSourcesFound: return 2 + case .serverError: return 3 + } + } + + var errorDescription: String { + + switch self { + case .invalidInputParam(let param): return "Invalid input param: \(param)" + case .unableToParseData(let data): return "Unable to parse object" + case .noSourcesFound: return "No source found to play content" + case .serverError(let info): return "Server Error: \(info)" + } + } + + var userInfo: [String: Any] { + return [String: Any]() + } + +} + +/************************************************************/ +// MARK: - PhoenixMediaProvider +/************************************************************/ + +/* Description + + Using Session provider will help you create MediaEntry in order to play content with the player + It's requestig the asset data and creating sources with relevant information for ex' contentURL, licenseURL, fiarPlay certificate and etc' + + #Example of code + ```` + let phoenixMediaProvider = PhoenixMediaProvider() + .set(type: AssetType.media) + .set(assetId: asset.assetID) + .set(fileIds: [file.fileID.stringValue]) + .set(networkProtocol: "https") + .set(playbackContextType: isTrailer ? PlaybackContextType.trailer : PlaybackContextType.playback) + .set(sessionProvider: PhoenixSessionManager.shared) + + phoenixMediaProvider.loadMedia(callback: { (media, error) in + + if let mediaEntry = media, error == nil { + self.player?.prepare(MediaConfig.config(mediaEntry: mediaEntry, startTime: params.startOver ? 0 : asset.currentMediaPositionInSeconds)) + }else{ + print("error loading asset: \(error?.localizedDescription)") + self.delegate?.corePlayer(self, didFailWith:LS("player_error_unable_to_load_entry")) + } + ```` +}) +*/ +@objc public class PhoenixMediaProvider: NSObject, MediaEntryProvider { + + var sessionProvider: SessionProvider? + var assetId: String? + var type: AssetType? + var formats: [String]? + var fileIds: [String]? + var playbackContextType: PlaybackContextType? + var executor: RequestExecutor? + var networkProtocol: String? + + public override init() { } + + /// - Parameter sessionProvider: This provider provider the ks for all wroking request. + /// If ks is nil, the provider will load the meida with anonymous ks + /// - Returns: Self ( so you con continue set other parameters after it ) + @discardableResult + @nonobjc public func set(sessionProvider: SessionProvider?) -> Self { + self.sessionProvider = sessionProvider + return self + } + + /// Required parameter + /// + /// - Parameter assetId: asset identifier + /// - Returns: Self + @discardableResult + @nonobjc public func set(assetId: String?) -> Self { + self.assetId = assetId + return self + } + + /// - Parameter type: Asset Object type if it is Media Or EPG + /// - Returns: Self + @discardableResult + @nonobjc public func set(type: AssetType?) -> Self { + self.type = type + return self + } + + /// - Parameter playbackContextType: Trailer/Playback/StartOver/Catchup + /// - Returns: Self + @discardableResult + @nonobjc public func set(playbackContextType: PlaybackContextType?) -> Self { + self.playbackContextType = playbackContextType + return self + } + + /// - Parameter formats: Asset's requested file formats, + /// According to this formats array order the sources will be ordered in the mediaEntry + /// According to this formats sources will be filtered when creating the mediaEntry + /// - Returns: Self + @discardableResult + @nonobjc public func set(formats: [String]?) -> Self { + self.formats = formats + return self + } + + /// - Parameter formats: Asset's requested file ids, + /// According to this files array order the sources will be ordered in the mediaEntry + /// According to this ids sources will be filtered when creating the mediaEntry + /// - Returns: Self + @discardableResult + @nonobjc public func set(fileIds: [String]?) -> Self { + self.fileIds = fileIds + return self + } + + /// - Parameter networkProtocol: http/https + /// - Returns: Self + @discardableResult + @nonobjc public func set(networkProtocol: String?) -> Self { + self.networkProtocol = networkProtocol + return self + } + + /// - Parameter executor: executor which will be used to send request. + /// default is USRExecutor + /// - Returns: Self + @discardableResult + @nonobjc public func set(executor: RequestExecutor?) -> Self { + self.executor = executor + return self + } + + let defaultProtocol = "https" + + /// This object is created before loading the media in order to make sure all required attributes are set and we are ready to load + struct LoaderInfo { + var sessionProvider: SessionProvider + var assetId: String + var assetType: AssetType + var formats: [String]? + var fileIds: [String]? + var playbackContextType: PlaybackContextType + var networkProtocol: String + var executor: RequestExecutor + + } + + @objc public func loadMedia(callback: @escaping (MediaEntry?, Error?) -> Void) { + guard let sessionProvider = self.sessionProvider else { + callback(nil, PhoenixMediaProviderError.invalidInputParam(param: "sessionProvider" ).asNSError ) + return + } + guard let assetId = self.assetId else { + callback(nil, PhoenixMediaProviderError.invalidInputParam(param: "assetId" ).asNSError) + return + } + guard let type = self.type else { + callback(nil, PhoenixMediaProviderError.invalidInputParam(param: "type" ).asNSError) + return + } + guard let contextType = self.playbackContextType else { + callback(nil, PhoenixMediaProviderError.invalidInputParam(param: "contextType" ).asNSError) + return + } + + let pr = self.networkProtocol ?? defaultProtocol + var executor: RequestExecutor = USRExecutor.shared + if let exe = self.executor { + executor = exe + } + + let loaderParams = LoaderInfo(sessionProvider: sessionProvider, assetId: assetId, assetType: type, formats: self.formats, fileIds: self.fileIds, playbackContextType: contextType, networkProtocol:pr, executor: executor) + + self.startLoad(loaderInfo: loaderParams, callback: callback) + } + + // This is not implemened yet + public func cancel() { + + } + + /// This method is creating the request in order to get playback context, when ks id nil we are adding anonymous login request so some times we will have just get context request and some times we will have multi request with getContext request + anonymouse login + /// - Parameters: + /// - ks: ks if exist + /// - loaderInfo: info regarding entry to load + /// - Returns: request builder + func loaderRequestBuilder(ks: String?, loaderInfo: LoaderInfo) -> KalturaRequestBuilder? { + + let playbackContextOptions = PlaybackContextOptions(playbackContextType: loaderInfo.playbackContextType, protocls: [loaderInfo.networkProtocol], assetFileIds: loaderInfo.fileIds) + + if let token = ks { + + let playbackContextRequest = OTTAssetService.getPlaybackContext(baseURL:loaderInfo.sessionProvider.serverURL, ks: token, assetId: loaderInfo.assetId, type: loaderInfo.assetType, playbackContextOptions: playbackContextOptions ) + return playbackContextRequest + } else { + + let anonymouseLoginRequest = OTTUserService.anonymousLogin(baseURL: loaderInfo.sessionProvider.serverURL, partnerId: loaderInfo.sessionProvider.partnerId) + let ks = "{1:result:ks}" + let playbackContextRequest = OTTAssetService.getPlaybackContext(baseURL:loaderInfo.sessionProvider.serverURL, ks: ks, assetId: loaderInfo.assetId, type: loaderInfo.assetType, playbackContextOptions: playbackContextOptions ) + + guard let req1 = anonymouseLoginRequest, let req2 = playbackContextRequest else { + return nil + } + + let multiRquest = KalturaMultiRequestBuilder(url: loaderInfo.sessionProvider.serverURL)?.setOTTBasicParams() + multiRquest?.add(request: req1).add(request: req2) + return multiRquest + + } + + } + + /// This method is called after all input is valid and we can start loading media + /// + /// - Parameters: + /// - loaderInfo: load info + /// - callback: completion clousor + func startLoad(loaderInfo: LoaderInfo, callback: @escaping (MediaEntry?, Error?) -> Void) { + loaderInfo.sessionProvider.loadKS { (ks, error) in + + guard let requestBuilder: KalturaRequestBuilder = self.loaderRequestBuilder( ks: ks, loaderInfo: loaderInfo) else { + callback(nil, PhoenixMediaProviderError.invalidInputParam(param:"requests params")) + return + } + + let isMultiRequest = requestBuilder is KalturaMultiRequestBuilder + + let request = requestBuilder.set(completion: { (response: Response) in + + var playbackContext: OTTBaseObject? = nil + do { + if (isMultiRequest) { + playbackContext = try OTTMultiResponseParser.parse(data: response.data).last + } else { + playbackContext = try OTTResponseParser.parse(data: response.data) + } + + } catch { + callback(nil, PhoenixMediaProviderError.unableToParseData(data:response.data).asNSError) + } + + if let context = playbackContext as? OTTPlaybackContext { + let media = self.createMediaEntry(loaderInfo: loaderInfo, context: context) + if let sources = media.sources, sources.count > 0 { + callback(media, nil) + } else { + callback(nil, PhoenixMediaProviderError.noSourcesFound.asNSError) + } + } else if let error = playbackContext as? OTTError { + callback(nil, PhoenixMediaProviderError.serverError(info: error.message ?? "Unknown Error").asNSError) + } else { + callback(nil, PhoenixMediaProviderError.unableToParseData(data: response.data).asNSError) + } + }).build() + + loaderInfo.executor.send(request: request) + } + } + + /// Sorting and filtering source accrding to file formats or file ids + func sortedAndFilterSources(by fileIds: [String]?, or fileFormats: [String]?, sources: [OTTPlaybackSource]) -> [OTTPlaybackSource] { + + let orderedSources = sources.filter({ (source: OTTPlaybackSource) -> Bool in + if let formats = fileFormats { + return formats.contains(source.type) + } else if let fileIds = fileIds { + return fileIds.contains("\(source.id)") + } else { + return true + } + }) + .sorted { (source1: OTTPlaybackSource, source2: OTTPlaybackSource) -> Bool in + + if let formats = fileFormats { + let index1 = formats.index(of: source1.type) ?? 0 + let index2 = formats.index(of: source2.type) ?? 0 + return index1 < index2 + } else if let fileIds = fileIds { + + let index1 = fileIds.index(of: "\(source1.id)") ?? 0 + let index2 = fileIds.index(of: "\(source2.id)") ?? 0 + return index1 < index2 + } else { + return false + } + } + + return orderedSources + + } + + func createMediaEntry(loaderInfo: LoaderInfo, context: OTTPlaybackContext) -> MediaEntry { + + let mediaEntry = MediaEntry(id: loaderInfo.assetId) + let sortedSources = self.sortedAndFilterSources(by: loaderInfo.fileIds, or: loaderInfo.formats, sources: context.sources) + + var maxDuration: Float = 0.0 + let mediaSources = sortedSources.flatMap { (source: OTTPlaybackSource) -> MediaSource? in + + let format = FormatsHelper.getMediaFormat(format: source.format, hasDrm: source.drm != nil) + guard FormatsHelper.supportedFormats.contains(format) else { + return nil + } + + var drm: [DRMParams]? = nil + if let drmData = source.drm, drmData.count > 0 { + drm = drmData.flatMap({ (drmData: OTTDrmData) -> DRMParams? in + + let scheme = self.convertScheme(scheme: drmData.scheme) + guard FormatsHelper.supportedSchemes.contains(scheme) else { + return nil + } + + switch scheme { + case .fairplay: + // if the scheme is type fair play and there is no certificate or license URL + guard let certifictae = drmData.certificate + else { return nil } + return FairPlayDRMParams(licenseUri: drmData.licenseURL, scheme: scheme, base64EncodedCertificate: certifictae) + default: + return DRMParams(licenseUri: drmData.licenseURL, scheme: scheme) + } + }) + + // checking if the source is supported with his drm data, cause if the source has drm data but from some reason the mapped drm data is empty the source is not playable + guard let mappedDrmData = drm, mappedDrmData.count > 0 else { + return nil + } + } + + let mediaSource = MediaSource(id: "\(source.id)") + mediaSource.contentUrl = source.url + mediaSource.mediaFormat = format + mediaSource.drmData = drm + + maxDuration = max(maxDuration, source.duration) + return mediaSource + + } + + mediaEntry.sources = mediaSources + mediaEntry.duration = TimeInterval(maxDuration) + + return mediaEntry + + } + + // Mapping between server scheme and local definision of scheme + func convertScheme(scheme: String) -> DRMParams.Scheme { + switch (scheme) { + case "WIDEVINE_CENC": + return .widevineCenc + case "PLAYREADY_CENC": + return .playreadyCenc + case "WIDEVINE": + return .widevineClassic + case "FAIRPLAY": + return .fairplay + default: + return .unknown + } + } + +} diff --git a/Classes/Providers/OTT/Services/OTTAssetService.swift b/Classes/Providers/OTT/Services/OTTAssetService.swift index 66040088..14852321 100644 --- a/Classes/Providers/OTT/Services/OTTAssetService.swift +++ b/Classes/Providers/OTT/Services/OTTAssetService.swift @@ -9,37 +9,53 @@ import UIKit import SwiftyJSON -@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 "" - } - } -} - class OTTAssetService { - static func get(baseURL: String, ks: String, assetId: String, type: AssetType) -> KalturaRequestBuilder? { - + 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: "assetReferenceType", value: JSON(type.asString)) .setBody(key: "type", value: JSON(type.rawValue)) - .setBody(key: "with", value: JSON([["type": "files","objectType": "KalturaCatalogWithHolder"]])) + .setBody(key: "assetReferenceType", value: JSON(type.rawValue)) + .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? { + + if let request: KalturaRequestBuilder = KalturaRequestBuilder(url: baseURL, service: "asset", action: "getPlaybackContext") { + request + .setBody(key: "assetId", value: JSON(assetId)) + .setBody(key: "ks", value: JSON(ks)) + .setBody(key: "assetType", value: JSON(type.rawValue)) + .setBody(key: "contextDataParams", value: JSON(playbackContextOptions.toDictionary())) + return request + } else { + return nil + } + + } } +struct PlaybackContextOptions { + internal var playbackContextType: PlaybackContextType + internal var protocls: [String] + internal var assetFileIds: [String]? + func toDictionary() -> [String: Any] { + + var dict: [String: Any] = [:] + dict["context"] = playbackContextType.rawValue + dict["mediaProtocols"] = protocls + if let fileIds = self.assetFileIds { + dict["assetFileIds"] = fileIds.joined(separator: ",") + } + return dict + } +} diff --git a/Classes/Providers/OTT/Services/OTTLicensedURLService.swift b/Classes/Providers/OTT/Services/OTTLicensedURLService.swift index 5fbee8ef..03a343dd 100644 --- a/Classes/Providers/OTT/Services/OTTLicensedURLService.swift +++ b/Classes/Providers/OTT/Services/OTTLicensedURLService.swift @@ -11,17 +11,16 @@ import SwiftyJSON class OTTLicensedURLService: NSObject { - - internal static func get(baseURL: String, ks: String, fileId: String, fileBaseURL:String) -> KalturaRequestBuilder? { - + 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{ + } else { return nil } } - + } diff --git a/Classes/Providers/OTT/Services/OTTSessionService.swift b/Classes/Providers/OTT/Services/OTTSessionService.swift index 1f29a248..bfd04165 100644 --- a/Classes/Providers/OTT/Services/OTTSessionService.swift +++ b/Classes/Providers/OTT/Services/OTTSessionService.swift @@ -11,17 +11,29 @@ import SwiftyJSON internal class OTTSessionService: NSObject { - - internal static func get(baseURL:String,ks:String) -> KalturaRequestBuilder? { - + 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{ + } 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 new file mode 100644 index 00000000..d490a8aa --- /dev/null +++ b/Classes/Providers/OTT/Services/OTTSocialService.swift @@ -0,0 +1,41 @@ +// +// 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 index 259f0940..596cd037 100644 --- a/Classes/Providers/OTT/Services/OTTUserService.swift +++ b/Classes/Providers/OTT/Services/OTTUserService.swift @@ -11,32 +11,57 @@ import SwiftyJSON public class OTTUserService: NSObject { - static func login(baseURL: String, partnerId: Int64, username: String, password: String) -> KalturaRequestBuilder? { - + 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 } - - static func refreshSession(baseURL: String, refreshToken: String, ks: String) -> KalturaRequestBuilder? { + + 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 } - - static func anonymousLogin(baseURL: String, partnerId: Int64) -> KalturaRequestBuilder? { + + 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/Services/PhoenixAPIDefines.swift b/Classes/Providers/OTT/Services/PhoenixAPIDefines.swift new file mode 100644 index 00000000..1257d623 --- /dev/null +++ b/Classes/Providers/OTT/Services/PhoenixAPIDefines.swift @@ -0,0 +1,22 @@ +// +// PhoenixAPIDefines.swift +// Pods +// +// Created by Rivka Peleg on 02/03/2017. +// +// + +import Foundation + +public enum AssetType: String { + case media = "media" + case epg = "epg" +} + +public enum PlaybackContextType: String { + + case trailer = "TRAILER" + case catchup = "CATCHUP" + case startOver = "START_OVER" + case playback = "PLAYBACK" +} diff --git a/Classes/Providers/OTT/Session/OTTSessionManager.swift b/Classes/Providers/OTT/Session/OTTSessionManager.swift index 5549aff5..eaa5f26e 100644 --- a/Classes/Providers/OTT/Session/OTTSessionManager.swift +++ b/Classes/Providers/OTT/Session/OTTSessionManager.swift @@ -8,188 +8,360 @@ 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 failedToGetKS + + enum SessionManagerError: Error { + case failed case failedToGetLoginResponse case failedToRefreshKS - case failedToBuildRefreshRequest - case invalidRefreshCallResponse - case noRefreshTokenOrTokenToRefresh - case failedToParseResponse - } - - let saftyMargin = 5*60.0 - + 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 - - private var ks: String? - private var refreshToken: String? - private var tokenExpiration: Date? - + + 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{ + 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) } - - @objc public func startSession(username: String, password: String, completion: @escaping (_ error: Error?) -> Void) -> Void { - + + /************************************************************/ + // 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) - + password: password, + udid: udid) + let sessionGetRequest = OTTSessionService.get(baseURL: self.serverURL, - ks:"1:result:loginSession:ks") - - if let r1 = loginRequestBuilder, let r2 = sessionGetRequest { - - let mrb = KalturaMultiRequestBuilder(url: self.serverURL)?.add(request: r1).add(request: r2).setOTTBasicParams() - mrb?.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.ks = loginObj.loginSession?.ks - self.refreshToken = loginObj.loginSession?.refreshToken - self.tokenExpiration = sessionObj.tokenExpiration - - } - completion(nil) - } else { - completion(SessionManagerError.failedToGetLoginResponse) - } - } else { - completion(SessionManagerError.failedToGetLoginResponse) - } - }) - - if let request = mrb?.build() { - self.executor.send(request: request) + 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 } } - - @objc public func startAnonymousSession(completion:@escaping (_ error: Error?) -> Void) { - + + /************************************************************/ + // 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") - - if let r1 = loginRequestBuilder, let r2 = sessionGetRequest { - - let mrb = KalturaMultiRequestBuilder(url: self.serverURL)?.add(request: r1) - .setOTTBasicParams() - .add(request: r2).setOTTBasicParams() - .set(completion: { (r:Response) in - - if let data = r.data - { + 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) + result = try OTTMultiResponseParser.parse(data:data) } catch { completion(error) } - - if let result = result, result.count == 2, let loginSession = result[0] as? OTTLoginSession, let session = result[1] as? OTTSession{ - - self.ks = loginSession.ks - self.refreshToken = loginSession.refreshToken - self.tokenExpiration = session.tokenExpiration - completion(nil) + + 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.failedToGetLoginResponse) + completion(SessionManagerError.failed) } } else { - completion(SessionManagerError.failedToGetLoginResponse) + completion(SessionManagerError.failed) } }) - - if let request = mrb?.build() { - self.executor.send(request: request) - } - } - } - - @objc public func loadKS(completion: @escaping (String?, Error?) -> Void) { - let now = Date() - - if let expiration = self.tokenExpiration, expiration.timeIntervalSince(now) > saftyMargin { - completion(self.ks, nil) - } else { - self.refreshKS(completion: completion) - } - } - - @objc public func refreshKS(completion: @escaping (String?, Error?) -> Void) { - - guard let refreshToken = self.refreshToken, let ks = self.ks else { - completion(nil, SessionManagerError.noRefreshTokenOrTokenToRefresh) - return - } - - let refreshSessionRequest = OTTUserService.refreshSession(baseURL: self.serverURL, refreshToken: refreshToken, ks: ks) - let getSessionRequest = OTTSessionService.get(baseURL: self.serverURL, ks: "1:result:ks") - - guard let req1 = refreshSessionRequest, let req2 = getSessionRequest else { - completion(nil, SessionManagerError.invalidRefreshCallResponse) - return - } - - let mrb: KalturaMultiRequestBuilder? = (KalturaMultiRequestBuilder(url: self.serverURL)?.add(request: req1).add(request: req2))?.setOTTBasicParams().set(completion: { (r:Response) in - - guard let data = r.data else { - completion(nil, SessionManagerError.failedToRefreshKS) - return - } - - var response: [OTTBaseObject]? = nil - do { - response = try OTTMultiResponseParser.parse(data: data) - } catch { - completion(nil, error) - } - - if let response = response, response.count == 2, let loginSession = response[0] as? OTTLoginSession, let session = response[1] as? OTTSession { - - self.ks = loginSession.ks - self.refreshToken = loginSession.refreshToken - self.tokenExpiration = session.tokenExpiration - completion(self.ks, nil) - } else { - completion(nil, SessionManagerError.failedToRefreshKS) - } - }) - - if let request = mrb?.build() { + + let request = request.build() self.executor.send(request: request) - } else { - completion(nil, SessionManagerError.failedToBuildRefreshRequest) - } } -} +} diff --git a/Classes/Providers/OVP/Model/OVPSource.swift b/Classes/Providers/OVP/Model/OVPSource.swift index 2aaf0f3e..3218267c 100644 --- a/Classes/Providers/OVP/Model/OVPSource.swift +++ b/Classes/Providers/OVP/Model/OVPSource.swift @@ -12,7 +12,7 @@ import SwiftyJSON class OVPSource: OVPBaseObject { var deliveryProfileId: Int64 - var format: String? + var format: String var protocols: [String]? var flavors: [String]? var url: URL? @@ -31,12 +31,14 @@ class OVPSource: OVPBaseObject { let jsonObject = JSON(json) - guard let id = jsonObject[deliveryProfileIdKey].int64 else { - return nil + guard let id = jsonObject[deliveryProfileIdKey].int64, + let format = jsonObject[formatKey].string + else { + return nil } self.deliveryProfileId = id - self.format = jsonObject[formatKey].string + self.format = format if let protocols = jsonObject[protocolsKey].string{ self.protocols = protocols.components(separatedBy: ",") } diff --git a/Classes/Providers/OVP/OVPMediaProvider.swift b/Classes/Providers/OVP/OVPMediaProvider.swift index c8915f30..5cefa1b2 100644 --- a/Classes/Providers/OVP/OVPMediaProvider.swift +++ b/Classes/Providers/OVP/OVPMediaProvider.swift @@ -184,7 +184,8 @@ import SwiftyXMLParser var mediaSources: [MediaSource] = [MediaSource]() sources.forEach { (source: OVPSource) in //detecting the source type - let format = self.getSourceFormat(source: source) + + let format = FormatsHelper.getMediaFormat(format: source.format, hasDrm: source.drm != nil) //If source type is not supported source will not be created guard format != .unknown else { return } @@ -255,26 +256,8 @@ import SwiftyXMLParser } - // This method decding the source type base on scheck and drm data - private func getSourceFormat(source: OVPSource) -> MediaSource.MediaFormat { - - if let format = source.format { - switch format { - case "applehttp": - return .hls - case "url": - if source.drm == nil { - return .mp4 - } else { - return .wvm - } - default: - return .unknown - } - } - - return .unknown - } + + // Creating the drm data based on scheme private func buildDRMParams(drm: [OVPDRM]?) -> [DRMParams]? { @@ -306,7 +289,7 @@ import SwiftyXMLParser // building the url with the SourceBuilder class private func playbackURL(loadInfo: LoaderInfo, source: OVPSource, ks: String?) -> URL? { - let formatType = self.getSourceFormat(source: source) + let formatType = FormatsHelper.getMediaFormat(format: source.format, hasDrm: source.drm != nil) var playURL: URL? = nil if let flavors = source.flavors, flavors.count > 0 { diff --git a/Example/PlayKit.xcodeproj/project.pbxproj b/Example/PlayKit.xcodeproj/project.pbxproj index f40b0675..b51da04c 100644 --- a/Example/PlayKit.xcodeproj/project.pbxproj +++ b/Example/PlayKit.xcodeproj/project.pbxproj @@ -22,14 +22,14 @@ 8460889C1DD1AB1A009E0E7A /* Entries.json in Resources */ = {isa = PBXBuildFile; fileRef = 8460889B1DD1AB1A009E0E7A /* Entries.json */; }; BF5C80821DF0E36500D3E665 /* MessegeBusTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5C80811DF0E36500D3E665 /* MessegeBusTest.swift */; }; C23A62D11DF413FF00635FA2 /* MockMediaProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62C91DF412FD00635FA2 /* MockMediaProviderTest.swift */; }; - C23A62D21DF4140B00635FA2 /* OTTMediaProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62CA1DF412FD00635FA2 /* OTTMediaProviderTest.swift */; }; - C23A62D31DF4140E00635FA2 /* OVPMediaProviederTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62CB1DF412FD00635FA2 /* OVPMediaProviederTest.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 */; }; + C2D803221E6C09CF00A3DE15 /* SourceSelectorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB09C99B1E28072900D3671F /* SourceSelectorTest.swift */; }; C63FA2B01DF3F854004030E0 /* PlayerControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63FA2AE1DF3F854004030E0 /* PlayerControllerTest.swift */; }; - FB09C99C1E28072900D3671F /* SourceSelectorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB09C99B1E28072900D3671F /* SourceSelectorTest.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -69,7 +69,7 @@ BE76A2A9EC2DDAC016A40200 /* Pods_PlayKit_PlayKit_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PlayKit_PlayKit_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BF5C80811DF0E36500D3E665 /* MessegeBusTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessegeBusTest.swift; sourceTree = ""; }; C23A62C91DF412FD00635FA2 /* MockMediaProviderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMediaProviderTest.swift; sourceTree = ""; }; - C23A62CA1DF412FD00635FA2 /* OTTMediaProviderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTTMediaProviderTest.swift; sourceTree = ""; }; + 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 = ""; }; @@ -242,7 +242,7 @@ children = ( C23A62DB1DF47D9800635FA2 /* OTTSessionProviderTest.swift */, C23A62C91DF412FD00635FA2 /* MockMediaProviderTest.swift */, - C23A62CA1DF412FD00635FA2 /* OTTMediaProviderTest.swift */, + C23A62CA1DF412FD00635FA2 /* PhoenixMediaProviderTest.swift */, C23A62CB1DF412FD00635FA2 /* OVPMediaProviederTest.swift */, C23A62CF1DF4131700635FA2 /* MediaEntryProviderMock.swift */, ); @@ -476,14 +476,14 @@ files = ( C23A62D41DF4141000635FA2 /* MediaEntryProviderMock.swift in Sources */, C23A62D11DF413FF00635FA2 /* MockMediaProviderTest.swift in Sources */, - C23A62D31DF4140E00635FA2 /* OVPMediaProviederTest.swift in Sources */, + C2D803211E6C091600A3DE15 /* OVPMediaProviederTest.swift in Sources */, 20CD64591E4C9697002C9401 /* PluginTestConfiguration.swift in Sources */, C63FA2B01DF3F854004030E0 /* PlayerControllerTest.swift in Sources */, + C2D803221E6C09CF00A3DE15 /* SourceSelectorTest.swift in Sources */, 2012AFB21E4B860B00BBA61C /* PhoenixPluginTest.swift in Sources */, BF5C80821DF0E36500D3E665 /* MessegeBusTest.swift in Sources */, 2012AFB41E4B872300BBA61C /* PlayerCreator.swift in Sources */, - FB09C99C1E28072900D3671F /* SourceSelectorTest.swift in Sources */, - C23A62D21DF4140B00635FA2 /* OTTMediaProviderTest.swift in Sources */, + C23A62D21DF4140B00635FA2 /* PhoenixMediaProviderTest.swift in Sources */, 2012AFAF1E4B85CA00BBA61C /* OTTAnalyticsPluginTest.swift in Sources */, C23A62DD1DF47D9C00635FA2 /* OTTSessionProviderTest.swift in Sources */, ); diff --git a/Example/Podfile.lock b/Example/Podfile.lock index a02d6c7c..bd46cf89 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -67,6 +67,6 @@ SPEC CHECKSUMS: Youbora-AVPlayer: 02aea2a12a4f7e6a61d8a1747e5dfc177bf2354b Youbora-YouboraLib: 523adf7cd09c4a213e3485e6ec7c48d498986246 -PODFILE CHECKSUM: 25eed3c6d61ea6aa08f2373344715de01155899a +PODFILE CHECKSUM: 8702b84cf4a02b3d2de2ccf0fe915659f256d0e6 COCOAPODS: 1.2.0.beta.1 diff --git a/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift b/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift index d2ce1d5d..74de74a3 100644 --- a/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift +++ b/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift @@ -42,9 +42,8 @@ class MockMediaProviderTest: XCTestCase { .set(url: self.filePath!) .set(id: "m001") - mediaProvider1.loadMedia { (r:Result) in - print(r) - if r.data != nil { + mediaProvider1.loadMedia { (media, error) in + if media != nil { theExeption.fulfill() } else{ @@ -64,8 +63,8 @@ class MockMediaProviderTest: XCTestCase { let theExeption = expectation(description: "test") let mediaProvider2 : MediaEntryProvider = MockMediaEntryProvider().set(url: self.filePath!).set(id: "sdf") - mediaProvider2.loadMedia { (r:Result) in - if r.error != nil { + mediaProvider2.loadMedia { (media, error) in + if error != nil { theExeption.fulfill() }else{ XCTFail() @@ -84,8 +83,8 @@ class MockMediaProviderTest: XCTestCase { let theExeption = expectation(description: "test") let mediaProvider2 : MediaEntryProvider = MockMediaEntryProvider().set(url: URL(string:"asdd")).set(id: "sdf") - mediaProvider2.loadMedia { (r:Result) in - if r.error != nil { + mediaProvider2.loadMedia { (media, error) in + if error != nil { theExeption.fulfill() }else{ XCTFail() @@ -106,8 +105,8 @@ class MockMediaProviderTest: XCTestCase { .set(content: self.fileContent) .set(id: "m001") - mediaProvider1.loadMedia { (r:Result) in - if let mediaEntry = r.data { + mediaProvider1.loadMedia { (media, error) in + if let mediaEntry = media { if let sources = mediaEntry.sources, sources.count > 0{ let source = sources[0] diff --git a/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift b/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift deleted file mode 100644 index cd0a3c77..00000000 --- a/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// OTTMediaProviderTest.swift -// PlayKit -// -// Created by Rivka Peleg on 04/12/2016. -// Copyright © 2016 CocoaPods. All rights reserved. -// - -import XCTest -import PlayKit - - - -class OTTMediaProviderTest: XCTestCase, SessionProvider { - - - - - let mediaID = "258656" - var partnerId: Int64 = 198 - var serverURL: String = "http://52.210.223.65:8080/v4_0/api_v3" - - public func loadKS(completion: @escaping (Result) -> Void) { - completion(Result(data: "djJ8MTk4fLsl2jWZfTLBHh80n32POkgauZLWcLXhEEySDRL9yRtOLtr92sPWaKpnCaz4nJgsjjXIxD6PkOLXlOvpEHV3Wizc384sF3F4Kj1MfiqJRQd8", error: nil)) - } - - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - - func testRegularCaseTest() { - - let theExeption = expectation(description: "test") - - let provider = OTTMediaProvider() - .set(sessionProvider: self) - .set(mediaId: mediaID) - .set(type: AssetType.media) - .set(formats: ["Mobile_Devices_Main_HD"]) - - provider.loadMedia { (r:Result) in - print(r) - if (r.error != nil){ - theExeption.fulfill() - }else{ - XCTFail() - } - } - - self.waitForExpectations(timeout: 6.0) { (_) -> Void in - - } - } -} - - diff --git a/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift b/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift index e305e734..82c44ae7 100644 --- a/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift +++ b/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift @@ -21,12 +21,11 @@ class OTTSessionProviderTest: XCTestCase { func testOTTSessionProvider() { - let sessionProvider = OTTSessionManager(serverURL:"http://52.210.223.65:8080/v4_0/api_v3", partnerId:198, executor: nil) + 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: { (r:Result) in - print(r.data) - + sessionProvider.loadKS(completion: { (ks, error) in + print(ks ?? "") }) }else{ diff --git a/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift b/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift index 2f0f0be2..85724b26 100644 --- a/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift +++ b/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift @@ -14,8 +14,8 @@ import PlayKit class OVPMediaProviederTest: XCTestCase, SessionProvider { - public func loadKS(completion: @escaping (Result) -> Void) { - completion(Result(data: "djJ8MjIyMjQwMXwdcXO1uXvBNZYxpUCxIGfEN120AWUJGJYCTt2qbhE3hCXa62-TGAOrxUtA0WwBGCqRreBaAzd2Dnejy9bYmcqtC1SxtCkZjw_jwoFd4Y3Cl-9hYgSCTcLRdqiePConBm8=", error: nil)) + public func loadKS(completion: @escaping (String?, Error?) -> Void) { + completion("", nil) } let entryID = "1_ytsd86sc" @@ -40,13 +40,13 @@ class OVPMediaProviederTest: XCTestCase, SessionProvider { .set(entryId: self.entryID) .set(executor: MediaEntryProviderMockExecutor(entryID: entryID, domain: "ovp")) - provider.loadMedia { (r:Result) in - if (r.error != nil){ + provider.loadMedia { (media, error) in + if (error != nil){ XCTFail() }else{ theExeption.fulfill() } - print(r) + } @@ -65,13 +65,12 @@ class OVPMediaProviederTest: XCTestCase, SessionProvider { .set(executor: USRExecutor.shared ) - provider.loadMedia { (r:Result) in - if (r.error != nil){ + provider.loadMedia { (media, error) in + if (error != nil){ XCTFail() }else{ theExeption.fulfill() } - print(r) } diff --git a/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift b/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift new file mode 100644 index 00000000..6c3252b4 --- /dev/null +++ b/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift @@ -0,0 +1,58 @@ +// +// PhoenixMediaProviderTest.swift +// PlayKit +// +// Created by Rivka Peleg on 04/12/2016. +// Copyright © 2016 CocoaPods. All rights reserved. +// + +import XCTest +import PlayKit + + + +class PhoenixMediaProviderTest: XCTestCase, SessionProvider { + + + + + let mediaID = "485293" + var partnerId: Int64 = 198 + var serverURL: String = "http://api-preprod.ott.kaltura.com/v4_2/api_v3" + + + public func loadKS(completion: @escaping (String?, Error?) -> Void) { + completion(nil, nil) + } + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testRegularCaseTest() { + + let theExeption = expectation(description: "test") + + let provider = PhoenixMediaProvider() + .set(sessionProvider: self) + .set(assetId: mediaID) + .set(type: AssetType.media) + .set(playbackContextType: PlaybackContextType.playback) + + + provider.loadMedia { (entry, error) in + print(entry ?? "") + theExeption.fulfill() + } + + self.waitForExpectations(timeout: 6.0) { (_) -> Void in + + } + } +} + + diff --git a/Example/Tests/MessegeBusTest.swift b/Example/Tests/MessegeBusTest.swift index ef9bf06e..8e4ffe4e 100644 --- a/Example/Tests/MessegeBusTest.swift +++ b/Example/Tests/MessegeBusTest.swift @@ -29,7 +29,9 @@ class MessegeBusTest: XCTestCase { entry["sources"] = sources let mediaConfig = MediaConfig(mediaEntry: MediaEntry(json: entry)) - self.player = PlayKitManager.shared.loadPlayer(pluginConfig: nil) + do{ + self.player = try PlayKitManager.shared.loadPlayer(pluginConfig: nil) + }catch{} self.player.prepare(mediaConfig) } diff --git a/Example/Tests/PlayerControllerTest.swift b/Example/Tests/PlayerControllerTest.swift index 065409c2..8b55b960 100644 --- a/Example/Tests/PlayerControllerTest.swift +++ b/Example/Tests/PlayerControllerTest.swift @@ -32,7 +32,11 @@ class PlayerControllerTest: XCTestCase { entry["sources"] = sources let mediaConfig = MediaConfig(mediaEntry: MediaEntry(json: entry)) - self.player = PlayKitManager.shared.loadPlayer(pluginConfig: nil) + do{ + self.player = try PlayKitManager.shared.loadPlayer(pluginConfig: nil) + } catch { + + } self.player.prepare(mediaConfig) } diff --git a/Example/Tests/PlayerCreator.swift b/Example/Tests/PlayerCreator.swift index 8183f720..b0ffa10c 100644 --- a/Example/Tests/PlayerCreator.swift +++ b/Example/Tests/PlayerCreator.swift @@ -41,20 +41,28 @@ extension PlayerCreator where Self: QuickSpec { entry["id"] = "test" entry["sources"] = sources let mediaConfig = MediaConfig(mediaEntry: MediaEntry(json: entry)) - + do{ let pluginConfig: PluginConfig? if let pluginConfigDict = pluginConfigDict { let pluginConfig = PluginConfig(config: pluginConfigDict) - player = PlayKitManager.shared.loadPlayer(pluginConfig: pluginConfig) as! PlayerLoader + + + player = try PlayKitManager.shared.loadPlayer(pluginConfig: pluginConfig) as! PlayerLoader } else { pluginConfig = nil - player = PlayKitManager.shared.loadPlayer(pluginConfig: pluginConfig) as! PlayerLoader + player = try PlayKitManager.shared.loadPlayer(pluginConfig: pluginConfig) as! PlayerLoader } - - if shouldStartPreparing { - player.prepare(mediaConfig) + + if shouldStartPreparing { + player.prepare(mediaConfig) + } + return player + }catch{ + } - return player + + return PlayerLoader() + } } From 44f2c93fe33dfd1baa01cd0f5bcb1dd2d221d0eb Mon Sep 17 00:00:00 2001 From: ElizaSapir Date: Mon, 27 Mar 2017 16:23:40 +0300 Subject: [PATCH 04/27] Revert "Fem 1165: Pheonix GetPlaybackContext (#112)" (#122) This reverts commit 47da8eca020c9a95faa6fff7cba368794da94986. --- Classes/PKError.swift | 2 +- Classes/Player/AssetBuilder.swift | 4 +- Classes/Player/MediaEntry.swift | 5 +- Classes/Providers/Base/FormatsHelper.swift | 32 -- Classes/Providers/MediaEntryProvider.swift | 10 +- .../Providers/Mock/MockMediaProvider.swift | 36 +- Classes/Providers/OTT/Model/OTTAsset.swift | 15 +- .../Providers/OTT/Model/OTTBaseObject.swift | 4 +- Classes/Providers/OTT/Model/OTTDrmData.swift | 31 -- Classes/Providers/OTT/Model/OTTError.swift | 22 +- Classes/Providers/OTT/Model/OTTFile.swift | 24 +- .../OTT/Model/OTTGetAssetResponse.swift | 9 +- .../Providers/OTT/Model/OTTLicensedURL.swift | 11 +- .../OTT/Model/OTTLoginResponse.swift | 10 +- .../Providers/OTT/Model/OTTLoginSession.swift | 8 +- .../OTT/Model/OTTPlaybackContext.swift | 24 - .../OTT/Model/OTTPlaybackSource.swift | 58 --- .../OTT/Model/OTTRefreshedSession.swift | 10 +- Classes/Providers/OTT/Model/OTTSession.swift | 11 +- Classes/Providers/OTT/OTTMediaProvider.swift | 156 ++++++ .../OTT/Parsers/OTTMultiResponseParser.swift | 18 +- .../OTT/Parsers/OTTObjectMapper.swift | 12 +- .../OTT/Parsers/OTTResponseParser.swift | 11 +- .../Providers/OTT/PhoenixMediaProvider.swift | 395 --------------- .../OTT/Services/OTTAssetService.swift | 52 +- .../OTT/Services/OTTLicensedURLService.swift | 9 +- .../OTT/Services/OTTSessionService.swift | 22 +- .../OTT/Services/OTTSocialService.swift | 41 -- .../OTT/Services/OTTUserService.swift | 39 +- .../OTT/Services/PhoenixAPIDefines.swift | 22 - .../OTT/Session/OTTSessionManager.swift | 464 ++++++------------ Classes/Providers/OVP/Model/OVPSource.swift | 10 +- Classes/Providers/OVP/OVPMediaProvider.swift | 27 +- Example/PlayKit.xcodeproj/project.pbxproj | 16 +- Example/Podfile.lock | 2 +- .../MockMediaProviderTest.swift | 17 +- .../OTTMediaProviderTest.swift | 60 +++ .../OTTSessionProviderTest.swift | 7 +- .../OVPMediaProviederTest.swift | 15 +- .../PhoenixMediaProviderTest.swift | 58 --- Example/Tests/MessegeBusTest.swift | 4 +- Example/Tests/PlayerControllerTest.swift | 6 +- Example/Tests/PlayerCreator.swift | 22 +- 43 files changed, 583 insertions(+), 1228 deletions(-) delete mode 100644 Classes/Providers/Base/FormatsHelper.swift delete mode 100644 Classes/Providers/OTT/Model/OTTDrmData.swift delete mode 100644 Classes/Providers/OTT/Model/OTTPlaybackContext.swift delete mode 100644 Classes/Providers/OTT/Model/OTTPlaybackSource.swift create mode 100644 Classes/Providers/OTT/OTTMediaProvider.swift delete mode 100644 Classes/Providers/OTT/PhoenixMediaProvider.swift delete mode 100644 Classes/Providers/OTT/Services/OTTSocialService.swift delete mode 100644 Classes/Providers/OTT/Services/PhoenixAPIDefines.swift create mode 100644 Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift delete mode 100644 Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift diff --git a/Classes/PKError.swift b/Classes/PKError.swift index 3e3cb0e5..ebcb963f 100644 --- a/Classes/PKError.swift +++ b/Classes/PKError.swift @@ -160,7 +160,7 @@ protocol PKError: Error, CustomStringConvertible { extension PKError { /// description string - public var description: String { + var description: String { return "\(type(of: self)) ,domain: \(type(of: self).domain), errorCode: \(self.code)" } diff --git a/Classes/Player/AssetBuilder.swift b/Classes/Player/AssetBuilder.swift index 928974d9..c8740a6e 100644 --- a/Classes/Player/AssetBuilder.swift +++ b/Classes/Player/AssetBuilder.swift @@ -99,7 +99,7 @@ enum AssetError : Error { class DRMSupport { // FairPlay is not available in simulators and before iOS8 static let fairplay: Bool = { - if !Platform.isSimulator, #available(iOS 8, *) { + if Platform.isSimulator, #available(iOS 8, *) { return true } else { return false @@ -108,7 +108,7 @@ class DRMSupport { // FairPlay is not available in simulators and is only downloadable in iOS10 and up. static let fairplayOffline: Bool = { - if !Platform.isSimulator, #available(iOS 10, *) { + if Platform.isSimulator, #available(iOS 10, *) { return true } else { return false diff --git a/Classes/Player/MediaEntry.swift b/Classes/Player/MediaEntry.swift index 958fb477..86b8b30b 100644 --- a/Classes/Player/MediaEntry.swift +++ b/Classes/Player/MediaEntry.swift @@ -15,7 +15,6 @@ func getJson(_ json: Any) -> JSON { @objc public enum MediaType: Int { case live - case vod case unknown } @@ -137,6 +136,7 @@ func getJson(_ json: Any) -> JSON { private let idKey: String = "id" private let contentUrlKey: String = "url" + private let mimeTypeKey: String = "mimeType" private let drmDataKey: String = "drmData" private let formatTypeKey: String = "sourceType" @@ -147,6 +147,7 @@ func getJson(_ json: Any) -> JSON { @objc public init(_ id: String, contentUrl: URL?, mimeType: String? = nil, drmData: [DRMParams]? = nil, mediaFormat: MediaFormat = .unknown) { self.id = id self.contentUrl = contentUrl + self.mimeType = mimeType self.drmData = drmData self.mediaFormat = mediaFormat } @@ -159,6 +160,8 @@ func getJson(_ json: Any) -> JSON { self.contentUrl = sj[contentUrlKey].url + self.mimeType = sj[mimeTypeKey].string + if let drmData = sj[drmDataKey].array { self.drmData = drmData.flatMap { DRMParams.fromJSON($0) } } diff --git a/Classes/Providers/Base/FormatsHelper.swift b/Classes/Providers/Base/FormatsHelper.swift deleted file mode 100644 index d0e2cb5d..00000000 --- a/Classes/Providers/Base/FormatsHelper.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// FormatsHelper.swift -// Pods -// -// Created by Rivka Peleg on 05/03/2017. -// -// - -import Foundation - -public class FormatsHelper { - - static let supportedFormats: [MediaSource.MediaFormat] = [.hls, .mp4, .wvm, .mp3] - static let supportedSchemes: [DRMParams.Scheme] = [.fairplay, .widevineClassic] - - static func getMediaFormat (format: String, hasDrm: Bool) -> MediaSource.MediaFormat { - - switch format { - case "applehttp": - return .hls - case "url": - if hasDrm { - return .wvm - } else { - return .mp4 - } - default: - return .unknown - } - } - -} diff --git a/Classes/Providers/MediaEntryProvider.swift b/Classes/Providers/MediaEntryProvider.swift index fe20b569..48fcb97d 100644 --- a/Classes/Providers/MediaEntryProvider.swift +++ b/Classes/Providers/MediaEntryProvider.swift @@ -8,6 +8,7 @@ import UIKit + @objc public protocol MediaEntryProvider { /** This method is triggering the creation of media base on custom parameters and actions. @@ -31,7 +32,12 @@ import UIKit */ func loadMedia(callback: @escaping (MediaEntry?, Error?) -> Void) - + + func cancel() - + } + + + + diff --git a/Classes/Providers/Mock/MockMediaProvider.swift b/Classes/Providers/Mock/MockMediaProvider.swift index 4a133e3c..ebf40b7f 100644 --- a/Classes/Providers/Mock/MockMediaProvider.swift +++ b/Classes/Providers/Mock/MockMediaProvider.swift @@ -10,52 +10,52 @@ import UIKit import SwiftyJSON @objc public class MockMediaEntryProvider: NSObject, MediaEntryProvider { - + public enum MockError: Error { case invalidParam(paramName:String) case fileIsEmptyOrNotFound case unableToParseJSON case mediaNotFound } - + @objc public var id: String? @objc public var url: URL? @objc public var content: Any? - + @discardableResult @nonobjc public func set(id: String?) -> Self { self.id = id return self } - + @discardableResult @nonobjc public func set(url: URL?) -> Self { self.url = url return self } - + @discardableResult @nonobjc public func set(content: Any?) -> Self { self.content = content return self } - - public override init() { - + + public override init(){ + } - + struct LoaderInfo { var id: String var content: JSON } @objc public func loadMedia(callback: @escaping (MediaEntry?, Error?) -> Void) { - + guard let id = self.id else { callback(nil, MockError.invalidParam(paramName: "id")) return } - + var json: JSON? = nil if let content = self.content { json = JSON(content) @@ -70,30 +70,30 @@ import SwiftyJSON } json = JSON(data: data as Data) } - + guard let jsonContent = json else { callback(nil, MockError.unableToParseJSON) return } - + let loderInfo = LoaderInfo(id: id, content: jsonContent) - + guard loderInfo.content != .null else { callback(nil, MockError.unableToParseJSON) return } - + let jsonObject: JSON = loderInfo.content[loderInfo.id] guard jsonObject != .null else { callback(nil, MockError.mediaNotFound) return } - + let mediaEntry = MediaEntry(json: jsonObject.object) callback(mediaEntry, nil) } - + public func cancel() { - + } } diff --git a/Classes/Providers/OTT/Model/OTTAsset.swift b/Classes/Providers/OTT/Model/OTTAsset.swift index 9dbbe7b3..af065b38 100644 --- a/Classes/Providers/OTT/Model/OTTAsset.swift +++ b/Classes/Providers/OTT/Model/OTTAsset.swift @@ -11,28 +11,29 @@ import SwiftyJSON internal class OTTAsset: OTTBaseObject { - internal var id: String + internal var id: String internal var files: [OTTFile]? - + private let idKey = "id" private let idfiles = "mediaFiles" - + internal 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) { + if let file = OTTFile(json: jsonFile.object){ self.files?.append(file) } } } } } + diff --git a/Classes/Providers/OTT/Model/OTTBaseObject.swift b/Classes/Providers/OTT/Model/OTTBaseObject.swift index 03bc15ac..654a285c 100644 --- a/Classes/Providers/OTT/Model/OTTBaseObject.swift +++ b/Classes/Providers/OTT/Model/OTTBaseObject.swift @@ -9,6 +9,8 @@ import UIKit protocol OTTBaseObject { - + init?(json:Any) } + + diff --git a/Classes/Providers/OTT/Model/OTTDrmData.swift b/Classes/Providers/OTT/Model/OTTDrmData.swift deleted file mode 100644 index 9ef4d02a..00000000 --- a/Classes/Providers/OTT/Model/OTTDrmData.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// OTTDrmPlaybackPluginData.swift -// Pods -// -// Created by Rivka Peleg on 05/03/2017. -// -// - -import Foundation -import SwiftyJSON - -class OTTDrmData: OTTBaseObject { - - var scheme: String - var licenseURL: String - var certificate: String? - - required init?(json: Any) { - let jsonObject = JSON(json) - - guard let scheme = jsonObject["scheme"].string, - let licenseURL = jsonObject["licenseURL"].string - else { - return nil - } - - self.scheme = scheme - self.licenseURL = licenseURL - self.certificate = jsonObject["certificate"].string - } -} diff --git a/Classes/Providers/OTT/Model/OTTError.swift b/Classes/Providers/OTT/Model/OTTError.swift index 25905394..6727f496 100644 --- a/Classes/Providers/OTT/Model/OTTError.swift +++ b/Classes/Providers/OTT/Model/OTTError.swift @@ -9,21 +9,27 @@ import UIKit import SwiftyJSON -class OTTError: OTTBaseObject { + +class OTTError: OTTBaseObject { + var message: String? var code: String? - + let errorKey = "error" let messageKey = "message" let codeKey = "code" - + + required init?(json: Any) { - + let jsonObj: JSON = JSON(json) - let errorDict = jsonObj[errorKey] - self.message = errorDict[messageKey].string - self.code = errorDict[codeKey].string + self.message = jsonObj[errorKey][messageKey].string + self.code = jsonObj[errorKey][codeKey].string } - + + init() { + + } + } diff --git a/Classes/Providers/OTT/Model/OTTFile.swift b/Classes/Providers/OTT/Model/OTTFile.swift index 698836e7..9ea06496 100644 --- a/Classes/Providers/OTT/Model/OTTFile.swift +++ b/Classes/Providers/OTT/Model/OTTFile.swift @@ -10,33 +10,33 @@ import UIKit import SwiftyJSON internal class OTTFile: OTTBaseObject { - + internal var id: String - internal var type: String? - internal var url: URL? - internal var duration: TimeInterval? - + internal var type: String? = nil + internal var url: URL? = nil + internal var duration: TimeInterval? = nil + private let idKey: String = "id" private let typeKey: String = "type" private let urlKey: String = "url" private let durationKey: String = "url" - internal init(id: String) { + 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 { + }else{ return nil } - + self.type = fileJosn[typeKey].string - if let contentURL = fileJosn[urlKey].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 index 36f8ee6d..7924d64d 100644 --- a/Classes/Providers/OTT/Model/OTTGetAssetResponse.swift +++ b/Classes/Providers/OTT/Model/OTTGetAssetResponse.swift @@ -11,14 +11,15 @@ import SwiftyJSON internal class OTTGetAssetResponse: OTTBaseObject { - internal var asset: OTTAsset? - + internal var asset: OTTAsset? = nil + 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 index ea40ec67..79f03709 100644 --- a/Classes/Providers/OTT/Model/OTTLicensedURL.swift +++ b/Classes/Providers/OTT/Model/OTTLicensedURL.swift @@ -12,18 +12,19 @@ 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 index 263147e1..0c3a1c10 100644 --- a/Classes/Providers/OTT/Model/OTTLoginResponse.swift +++ b/Classes/Providers/OTT/Model/OTTLoginResponse.swift @@ -10,16 +10,16 @@ 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 index 4dc676b0..c1e652ef 100644 --- a/Classes/Providers/OTT/Model/OTTLoginSession.swift +++ b/Classes/Providers/OTT/Model/OTTLoginSession.swift @@ -13,15 +13,15 @@ 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/OTTPlaybackContext.swift b/Classes/Providers/OTT/Model/OTTPlaybackContext.swift deleted file mode 100644 index 530e4557..00000000 --- a/Classes/Providers/OTT/Model/OTTPlaybackContext.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// OTTPlaybackContext.swift -// Pods -// -// Created by Rivka Peleg on 05/03/2017. -// -// - -import Foundation -import SwiftyJSON - -class OTTPlaybackContext: OTTBaseObject { - - var sources: [OTTPlaybackSource] = [] - - required init?(json: Any) { - let jsonObject = JSON(json) - jsonObject["sources"].array?.forEach { (source: JSON) in - if let source = OTTPlaybackSource(json: source.object) { - sources.append(source) - } - } - } -} diff --git a/Classes/Providers/OTT/Model/OTTPlaybackSource.swift b/Classes/Providers/OTT/Model/OTTPlaybackSource.swift deleted file mode 100644 index e7f353f0..00000000 --- a/Classes/Providers/OTT/Model/OTTPlaybackSource.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// OTTPlaybackSource.swift -// Pods -// -// Created by Rivka Peleg on 05/03/2017. -// -// - -import Foundation -import SwiftyJSON - -class OTTPlaybackSource: OTTBaseObject { - - var assetId: Int - var id: Int - var type: String // file format - var url: URL? - var duration: Float - var externalId: String? - var protocols: [String] - var format: String - var drm: [OTTDrmData]? - - required init?(json: Any) { - let jsonObject = JSON(json) - - guard let assetId = jsonObject["assetId"].int, - let id = jsonObject["id"].int, - let type = jsonObject["type"].string, - let urlString = jsonObject["url"].string, - let protocolsString = jsonObject["protocols"].string, - let format = jsonObject["format"].string - else { - return nil - } - - self.assetId = assetId - self.id = id - self.type = type - self.url = URL.init(string: urlString) - self.protocols = protocolsString.components(separatedBy: ",") - self.format = format - self.duration = jsonObject["duration"].float ?? 0 - self.externalId = jsonObject["externalId"].string - - var drmArray = [OTTDrmData]() - jsonObject["drm"].array?.forEach {(json) in - if let drmObject = OTTDrmData(json: json.object) { - drmArray.append(drmObject) - } - } - - if drmArray.count > 0 { - self.drm = drmArray - } - - } -} diff --git a/Classes/Providers/OTT/Model/OTTRefreshedSession.swift b/Classes/Providers/OTT/Model/OTTRefreshedSession.swift index eb143544..e61fb46b 100644 --- a/Classes/Providers/OTT/Model/OTTRefreshedSession.swift +++ b/Classes/Providers/OTT/Model/OTTRefreshedSession.swift @@ -13,15 +13,17 @@ 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 index 60240116..f6dd9716 100644 --- a/Classes/Providers/OTT/Model/OTTSession.swift +++ b/Classes/Providers/OTT/Model/OTTSession.swift @@ -9,21 +9,18 @@ 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 { + if let time = jsonObject[tokenExpirationKey].number?.doubleValue{ self.tokenExpiration = Date.init(timeIntervalSince1970:time) } - self.udid = jsonObject[udidKey].string - } } diff --git a/Classes/Providers/OTT/OTTMediaProvider.swift b/Classes/Providers/OTT/OTTMediaProvider.swift new file mode 100644 index 00000000..70ec8f13 --- /dev/null +++ b/Classes/Providers/OTT/OTTMediaProvider.swift @@ -0,0 +1,156 @@ +// +// OTTEntryProvider.swift +// Pods +// +// Created by Admin on 13/11/2016. +// +// + +import UIKit +import SwiftyJSON + +@objc public class OTTMediaProvider: NSObject, MediaEntryProvider { + + public enum OTTMediaProviderError: Error { + case invalidInputParams + case invalidKS + case fileIsEmptyOrNotFound + case invalidJSON + case mediaNotFound + case currentlyProcessingOtherRequest + case unableToParseObject + } + + @objc public var sessionProvider: SessionProvider? + @objc public var mediaId: String? + @objc public var type: AssetType = .unknown + @objc public var formats: [String]? + public var executor: RequestExecutor? // TODO: make @objc if needed in the future + + public override init() {} + + @objc public init(_ sessionProvider: SessionProvider) { + self.sessionProvider = sessionProvider + } + + @discardableResult + @nonobjc public func set(sessionProvider: SessionProvider?) -> Self { + self.sessionProvider = sessionProvider + return self + } + + @discardableResult + @nonobjc public func set(mediaId:String?) -> Self { + self.mediaId = mediaId + return self + } + + @discardableResult + @nonobjc public func set(type: AssetType) -> Self { + self.type = type + return self + } + + @discardableResult + @nonobjc public func set(formats:[String]?) -> Self { + self.formats = formats + return self + } + + @discardableResult + @nonobjc public func set(executor:RequestExecutor?) -> Self { + self.executor = executor + return self + } + + struct LoaderInfo { + var sessionProvider: SessionProvider + var mediaId: String + var type: AssetType + var formats: [String] + var executor: RequestExecutor + } + + @objc public func loadMedia(callback: @escaping (MediaEntry?, Error?) -> Void) { + guard let sessionProvider = self.sessionProvider, + let mediaId = self.mediaId, + self.type != .unknown + else { + callback(nil, OTTMediaProviderError.invalidInputParams) + return + } + + var executor: RequestExecutor = USRExecutor.shared + var formats: [String] = [] + if let exe = self.executor{ + executor = exe + } + + if let fmts = self.formats { + formats = fmts + } + + let loaderParams = LoaderInfo(sessionProvider: sessionProvider, mediaId: mediaId, type: type, formats: formats, executor: executor) + self.startLoad(loader: loaderParams, callback: callback) + } + + public func cancel() { + + } + + func startLoad(loader: LoaderInfo, callback: @escaping (MediaEntry?, Error?) -> Void) { + loader.sessionProvider.loadKS { (ks, error) in + guard let ks = ks else { + callback(nil, OTTMediaProviderError.invalidKS) + return + } + + let requestBuilder = OTTAssetService.get(baseURL: loader.sessionProvider.serverURL, ks: ks, assetId: loader.mediaId, type:loader.type)? + .setOTTBasicParams() + .set(completion: { (r:Response) in + + guard let data = r.data else { + callback(nil, OTTMediaProviderError.mediaNotFound) + return + } + + var object: OTTBaseObject? = nil + do { + object = try OTTResponseParser.parse(data: data) + } catch { + callback(nil, error) + } + + if let asset = object as? OTTAsset { + + let mediaEntry: MediaEntry = MediaEntry(id: asset.id) + if let files = asset.files { + + var sources = [MediaSource]() + for file in files { + if let fileFormat = file.type{ + if loader.formats.contains(fileFormat) == true { + let source: MediaSource = MediaSource(id: file.id) + source.contentUrl = file.url + sources.append(source) + + } + } + } + + if sources.count > 0 { + mediaEntry.sources = sources + } + } + callback(mediaEntry, nil) + } else { + callback(nil, OTTMediaProviderError.mediaNotFound) + } + }) + if let assetRequest = requestBuilder?.build() { + loader.executor.send(request: assetRequest) + } + } + } +} + diff --git a/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift b/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift index 88822a76..f5195ea0 100644 --- a/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift +++ b/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift @@ -10,33 +10,33 @@ import UIKit import SwiftyJSON class OTTMultiResponseParser: NSObject { - + enum OTTMultiResponseParserError: Error { case typeNotFound case emptyResponse case notMultiResponse } - + static func parse(data:Any) throws -> [OTTBaseObject] { - + let jsonResponse = JSON(data) if let resultArrayJSON = jsonResponse["result"].array { - + var resultArray: [OTTBaseObject] = [OTTBaseObject]() - for jsonObject: JSON in resultArrayJSON { + for jsonObject: JSON in resultArrayJSON{ var object: OTTBaseObject? = nil let objectType: OTTBaseObject.Type? = OTTObjectMapper.classByJsonObject(json: jsonObject.dictionaryObject) - if let type = objectType { + if let type = objectType{ object = type.init(json: jsonObject.object) } else { throw OTTMultiResponseParserError.typeNotFound } - - if let obj = object { + + if let obj = object{ resultArray.append(obj) } } - + return resultArray } else { return [OTTBaseObject]() diff --git a/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift b/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift index eb5250ab..0c81250e 100644 --- a/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift +++ b/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift @@ -9,16 +9,18 @@ import UIKit import SwiftyJSON + + class OTTObjectMapper: NSObject { static let classNameKey = "objectType" - static let errorKey = "error" - + static let errorKey = "objectType" + static func classByJsonObject(json: Any?) -> OTTBaseObject.Type? { guard let js = json else { return nil } let jsonObject = JSON(js) let className = jsonObject[classNameKey].string - + if let name = className { switch name { case "KalturaLoginResponse": @@ -29,10 +31,6 @@ class OTTObjectMapper: NSObject { return OTTAsset.self case "KalturaLoginSession": return OTTLoginSession.self - case "KalturaPlaybackSource": - return OTTPlaybackSource.self - case "KalturaPlaybackContext": - return OTTPlaybackContext.self default: return nil } diff --git a/Classes/Providers/OTT/Parsers/OTTResponseParser.swift b/Classes/Providers/OTT/Parsers/OTTResponseParser.swift index f771ee7d..eb6bcadf 100644 --- a/Classes/Providers/OTT/Parsers/OTTResponseParser.swift +++ b/Classes/Providers/OTT/Parsers/OTTResponseParser.swift @@ -10,14 +10,14 @@ import UIKit import SwiftyJSON class OTTResponseParser: ResponseParser { - + enum OTTResponseParserError: Error { case typeNotFound case invalidJsonObject } - + static func parse(data:Any) throws -> OTTBaseObject { - + let jsonResponse = JSON(data) let resultObjectJSON = jsonResponse["result"].dictionaryObject let objectType: OTTBaseObject.Type? = OTTObjectMapper.classByJsonObject(json: resultObjectJSON) @@ -27,8 +27,11 @@ class OTTResponseParser: ResponseParser { } else { throw OTTResponseParserError.invalidJsonObject } - } else { + }else{ throw OTTResponseParserError.typeNotFound } } } + + + diff --git a/Classes/Providers/OTT/PhoenixMediaProvider.swift b/Classes/Providers/OTT/PhoenixMediaProvider.swift deleted file mode 100644 index dc025273..00000000 --- a/Classes/Providers/OTT/PhoenixMediaProvider.swift +++ /dev/null @@ -1,395 +0,0 @@ -// -// OTTEntryProvider.swift -// -// -// Created by Admin on 13/11/2016. -// -// - -import UIKit -import SwiftyJSON - -/************************************************************/ -// MARK: - PhoenixMediaProviderError -/************************************************************/ -public enum PhoenixMediaProviderError: PKError { - - case invalidInputParam(param: String) - case unableToParseData(data: Any) - case noSourcesFound - case serverError(info:String) - - static let domain = "com.kaltura.playkit.error.PhoenixMediaProvider" - - var code: Int { - switch self { - case .invalidInputParam: return 0 - case .unableToParseData: return 1 - case .noSourcesFound: return 2 - case .serverError: return 3 - } - } - - var errorDescription: String { - - switch self { - case .invalidInputParam(let param): return "Invalid input param: \(param)" - case .unableToParseData(let data): return "Unable to parse object" - case .noSourcesFound: return "No source found to play content" - case .serverError(let info): return "Server Error: \(info)" - } - } - - var userInfo: [String: Any] { - return [String: Any]() - } - -} - -/************************************************************/ -// MARK: - PhoenixMediaProvider -/************************************************************/ - -/* Description - - Using Session provider will help you create MediaEntry in order to play content with the player - It's requestig the asset data and creating sources with relevant information for ex' contentURL, licenseURL, fiarPlay certificate and etc' - - #Example of code - ```` - let phoenixMediaProvider = PhoenixMediaProvider() - .set(type: AssetType.media) - .set(assetId: asset.assetID) - .set(fileIds: [file.fileID.stringValue]) - .set(networkProtocol: "https") - .set(playbackContextType: isTrailer ? PlaybackContextType.trailer : PlaybackContextType.playback) - .set(sessionProvider: PhoenixSessionManager.shared) - - phoenixMediaProvider.loadMedia(callback: { (media, error) in - - if let mediaEntry = media, error == nil { - self.player?.prepare(MediaConfig.config(mediaEntry: mediaEntry, startTime: params.startOver ? 0 : asset.currentMediaPositionInSeconds)) - }else{ - print("error loading asset: \(error?.localizedDescription)") - self.delegate?.corePlayer(self, didFailWith:LS("player_error_unable_to_load_entry")) - } - ```` -}) -*/ -@objc public class PhoenixMediaProvider: NSObject, MediaEntryProvider { - - var sessionProvider: SessionProvider? - var assetId: String? - var type: AssetType? - var formats: [String]? - var fileIds: [String]? - var playbackContextType: PlaybackContextType? - var executor: RequestExecutor? - var networkProtocol: String? - - public override init() { } - - /// - Parameter sessionProvider: This provider provider the ks for all wroking request. - /// If ks is nil, the provider will load the meida with anonymous ks - /// - Returns: Self ( so you con continue set other parameters after it ) - @discardableResult - @nonobjc public func set(sessionProvider: SessionProvider?) -> Self { - self.sessionProvider = sessionProvider - return self - } - - /// Required parameter - /// - /// - Parameter assetId: asset identifier - /// - Returns: Self - @discardableResult - @nonobjc public func set(assetId: String?) -> Self { - self.assetId = assetId - return self - } - - /// - Parameter type: Asset Object type if it is Media Or EPG - /// - Returns: Self - @discardableResult - @nonobjc public func set(type: AssetType?) -> Self { - self.type = type - return self - } - - /// - Parameter playbackContextType: Trailer/Playback/StartOver/Catchup - /// - Returns: Self - @discardableResult - @nonobjc public func set(playbackContextType: PlaybackContextType?) -> Self { - self.playbackContextType = playbackContextType - return self - } - - /// - Parameter formats: Asset's requested file formats, - /// According to this formats array order the sources will be ordered in the mediaEntry - /// According to this formats sources will be filtered when creating the mediaEntry - /// - Returns: Self - @discardableResult - @nonobjc public func set(formats: [String]?) -> Self { - self.formats = formats - return self - } - - /// - Parameter formats: Asset's requested file ids, - /// According to this files array order the sources will be ordered in the mediaEntry - /// According to this ids sources will be filtered when creating the mediaEntry - /// - Returns: Self - @discardableResult - @nonobjc public func set(fileIds: [String]?) -> Self { - self.fileIds = fileIds - return self - } - - /// - Parameter networkProtocol: http/https - /// - Returns: Self - @discardableResult - @nonobjc public func set(networkProtocol: String?) -> Self { - self.networkProtocol = networkProtocol - return self - } - - /// - Parameter executor: executor which will be used to send request. - /// default is USRExecutor - /// - Returns: Self - @discardableResult - @nonobjc public func set(executor: RequestExecutor?) -> Self { - self.executor = executor - return self - } - - let defaultProtocol = "https" - - /// This object is created before loading the media in order to make sure all required attributes are set and we are ready to load - struct LoaderInfo { - var sessionProvider: SessionProvider - var assetId: String - var assetType: AssetType - var formats: [String]? - var fileIds: [String]? - var playbackContextType: PlaybackContextType - var networkProtocol: String - var executor: RequestExecutor - - } - - @objc public func loadMedia(callback: @escaping (MediaEntry?, Error?) -> Void) { - guard let sessionProvider = self.sessionProvider else { - callback(nil, PhoenixMediaProviderError.invalidInputParam(param: "sessionProvider" ).asNSError ) - return - } - guard let assetId = self.assetId else { - callback(nil, PhoenixMediaProviderError.invalidInputParam(param: "assetId" ).asNSError) - return - } - guard let type = self.type else { - callback(nil, PhoenixMediaProviderError.invalidInputParam(param: "type" ).asNSError) - return - } - guard let contextType = self.playbackContextType else { - callback(nil, PhoenixMediaProviderError.invalidInputParam(param: "contextType" ).asNSError) - return - } - - let pr = self.networkProtocol ?? defaultProtocol - var executor: RequestExecutor = USRExecutor.shared - if let exe = self.executor { - executor = exe - } - - let loaderParams = LoaderInfo(sessionProvider: sessionProvider, assetId: assetId, assetType: type, formats: self.formats, fileIds: self.fileIds, playbackContextType: contextType, networkProtocol:pr, executor: executor) - - self.startLoad(loaderInfo: loaderParams, callback: callback) - } - - // This is not implemened yet - public func cancel() { - - } - - /// This method is creating the request in order to get playback context, when ks id nil we are adding anonymous login request so some times we will have just get context request and some times we will have multi request with getContext request + anonymouse login - /// - Parameters: - /// - ks: ks if exist - /// - loaderInfo: info regarding entry to load - /// - Returns: request builder - func loaderRequestBuilder(ks: String?, loaderInfo: LoaderInfo) -> KalturaRequestBuilder? { - - let playbackContextOptions = PlaybackContextOptions(playbackContextType: loaderInfo.playbackContextType, protocls: [loaderInfo.networkProtocol], assetFileIds: loaderInfo.fileIds) - - if let token = ks { - - let playbackContextRequest = OTTAssetService.getPlaybackContext(baseURL:loaderInfo.sessionProvider.serverURL, ks: token, assetId: loaderInfo.assetId, type: loaderInfo.assetType, playbackContextOptions: playbackContextOptions ) - return playbackContextRequest - } else { - - let anonymouseLoginRequest = OTTUserService.anonymousLogin(baseURL: loaderInfo.sessionProvider.serverURL, partnerId: loaderInfo.sessionProvider.partnerId) - let ks = "{1:result:ks}" - let playbackContextRequest = OTTAssetService.getPlaybackContext(baseURL:loaderInfo.sessionProvider.serverURL, ks: ks, assetId: loaderInfo.assetId, type: loaderInfo.assetType, playbackContextOptions: playbackContextOptions ) - - guard let req1 = anonymouseLoginRequest, let req2 = playbackContextRequest else { - return nil - } - - let multiRquest = KalturaMultiRequestBuilder(url: loaderInfo.sessionProvider.serverURL)?.setOTTBasicParams() - multiRquest?.add(request: req1).add(request: req2) - return multiRquest - - } - - } - - /// This method is called after all input is valid and we can start loading media - /// - /// - Parameters: - /// - loaderInfo: load info - /// - callback: completion clousor - func startLoad(loaderInfo: LoaderInfo, callback: @escaping (MediaEntry?, Error?) -> Void) { - loaderInfo.sessionProvider.loadKS { (ks, error) in - - guard let requestBuilder: KalturaRequestBuilder = self.loaderRequestBuilder( ks: ks, loaderInfo: loaderInfo) else { - callback(nil, PhoenixMediaProviderError.invalidInputParam(param:"requests params")) - return - } - - let isMultiRequest = requestBuilder is KalturaMultiRequestBuilder - - let request = requestBuilder.set(completion: { (response: Response) in - - var playbackContext: OTTBaseObject? = nil - do { - if (isMultiRequest) { - playbackContext = try OTTMultiResponseParser.parse(data: response.data).last - } else { - playbackContext = try OTTResponseParser.parse(data: response.data) - } - - } catch { - callback(nil, PhoenixMediaProviderError.unableToParseData(data:response.data).asNSError) - } - - if let context = playbackContext as? OTTPlaybackContext { - let media = self.createMediaEntry(loaderInfo: loaderInfo, context: context) - if let sources = media.sources, sources.count > 0 { - callback(media, nil) - } else { - callback(nil, PhoenixMediaProviderError.noSourcesFound.asNSError) - } - } else if let error = playbackContext as? OTTError { - callback(nil, PhoenixMediaProviderError.serverError(info: error.message ?? "Unknown Error").asNSError) - } else { - callback(nil, PhoenixMediaProviderError.unableToParseData(data: response.data).asNSError) - } - }).build() - - loaderInfo.executor.send(request: request) - } - } - - /// Sorting and filtering source accrding to file formats or file ids - func sortedAndFilterSources(by fileIds: [String]?, or fileFormats: [String]?, sources: [OTTPlaybackSource]) -> [OTTPlaybackSource] { - - let orderedSources = sources.filter({ (source: OTTPlaybackSource) -> Bool in - if let formats = fileFormats { - return formats.contains(source.type) - } else if let fileIds = fileIds { - return fileIds.contains("\(source.id)") - } else { - return true - } - }) - .sorted { (source1: OTTPlaybackSource, source2: OTTPlaybackSource) -> Bool in - - if let formats = fileFormats { - let index1 = formats.index(of: source1.type) ?? 0 - let index2 = formats.index(of: source2.type) ?? 0 - return index1 < index2 - } else if let fileIds = fileIds { - - let index1 = fileIds.index(of: "\(source1.id)") ?? 0 - let index2 = fileIds.index(of: "\(source2.id)") ?? 0 - return index1 < index2 - } else { - return false - } - } - - return orderedSources - - } - - func createMediaEntry(loaderInfo: LoaderInfo, context: OTTPlaybackContext) -> MediaEntry { - - let mediaEntry = MediaEntry(id: loaderInfo.assetId) - let sortedSources = self.sortedAndFilterSources(by: loaderInfo.fileIds, or: loaderInfo.formats, sources: context.sources) - - var maxDuration: Float = 0.0 - let mediaSources = sortedSources.flatMap { (source: OTTPlaybackSource) -> MediaSource? in - - let format = FormatsHelper.getMediaFormat(format: source.format, hasDrm: source.drm != nil) - guard FormatsHelper.supportedFormats.contains(format) else { - return nil - } - - var drm: [DRMParams]? = nil - if let drmData = source.drm, drmData.count > 0 { - drm = drmData.flatMap({ (drmData: OTTDrmData) -> DRMParams? in - - let scheme = self.convertScheme(scheme: drmData.scheme) - guard FormatsHelper.supportedSchemes.contains(scheme) else { - return nil - } - - switch scheme { - case .fairplay: - // if the scheme is type fair play and there is no certificate or license URL - guard let certifictae = drmData.certificate - else { return nil } - return FairPlayDRMParams(licenseUri: drmData.licenseURL, scheme: scheme, base64EncodedCertificate: certifictae) - default: - return DRMParams(licenseUri: drmData.licenseURL, scheme: scheme) - } - }) - - // checking if the source is supported with his drm data, cause if the source has drm data but from some reason the mapped drm data is empty the source is not playable - guard let mappedDrmData = drm, mappedDrmData.count > 0 else { - return nil - } - } - - let mediaSource = MediaSource(id: "\(source.id)") - mediaSource.contentUrl = source.url - mediaSource.mediaFormat = format - mediaSource.drmData = drm - - maxDuration = max(maxDuration, source.duration) - return mediaSource - - } - - mediaEntry.sources = mediaSources - mediaEntry.duration = TimeInterval(maxDuration) - - return mediaEntry - - } - - // Mapping between server scheme and local definision of scheme - func convertScheme(scheme: String) -> DRMParams.Scheme { - switch (scheme) { - case "WIDEVINE_CENC": - return .widevineCenc - case "PLAYREADY_CENC": - return .playreadyCenc - case "WIDEVINE": - return .widevineClassic - case "FAIRPLAY": - return .fairplay - default: - return .unknown - } - } - -} diff --git a/Classes/Providers/OTT/Services/OTTAssetService.swift b/Classes/Providers/OTT/Services/OTTAssetService.swift index 14852321..66040088 100644 --- a/Classes/Providers/OTT/Services/OTTAssetService.swift +++ b/Classes/Providers/OTT/Services/OTTAssetService.swift @@ -9,53 +9,37 @@ import UIKit import SwiftyJSON -class OTTAssetService { +@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 "" + } + } +} - internal static func get(baseURL: String, ks: String, assetId: String, type: AssetType) -> KalturaRequestBuilder? { +class OTTAssetService { + 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: "assetReferenceType", value: JSON(type.asString)) .setBody(key: "type", value: JSON(type.rawValue)) - .setBody(key: "assetReferenceType", value: JSON(type.rawValue)) - .setBody(key: "with", value: JSON([["type": "files", "objectType": "KalturaCatalogWithHolder"]])) + .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? { - - if let request: KalturaRequestBuilder = KalturaRequestBuilder(url: baseURL, service: "asset", action: "getPlaybackContext") { - request - .setBody(key: "assetId", value: JSON(assetId)) - .setBody(key: "ks", value: JSON(ks)) - .setBody(key: "assetType", value: JSON(type.rawValue)) - .setBody(key: "contextDataParams", value: JSON(playbackContextOptions.toDictionary())) - return request - } else { - return nil - } - - } } -struct PlaybackContextOptions { - internal var playbackContextType: PlaybackContextType - internal var protocls: [String] - internal var assetFileIds: [String]? - func toDictionary() -> [String: Any] { - - var dict: [String: Any] = [:] - dict["context"] = playbackContextType.rawValue - dict["mediaProtocols"] = protocls - if let fileIds = self.assetFileIds { - dict["assetFileIds"] = fileIds.joined(separator: ",") - } - return dict - } -} diff --git a/Classes/Providers/OTT/Services/OTTLicensedURLService.swift b/Classes/Providers/OTT/Services/OTTLicensedURLService.swift index 03a343dd..5fbee8ef 100644 --- a/Classes/Providers/OTT/Services/OTTLicensedURLService.swift +++ b/Classes/Providers/OTT/Services/OTTLicensedURLService.swift @@ -11,16 +11,17 @@ import SwiftyJSON class OTTLicensedURLService: NSObject { - internal static func get(baseURL: String, ks: String, fileId: String, fileBaseURL: String) -> KalturaRequestBuilder? { - + + 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 { + }else{ return nil } } - + } diff --git a/Classes/Providers/OTT/Services/OTTSessionService.swift b/Classes/Providers/OTT/Services/OTTSessionService.swift index bfd04165..1f29a248 100644 --- a/Classes/Providers/OTT/Services/OTTSessionService.swift +++ b/Classes/Providers/OTT/Services/OTTSessionService.swift @@ -11,29 +11,17 @@ import SwiftyJSON internal class OTTSessionService: NSObject { - internal static func get(baseURL: String, ks: String) -> KalturaRequestBuilder? { - + + 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 { + }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 index 596cd037..259f0940 100644 --- a/Classes/Providers/OTT/Services/OTTUserService.swift +++ b/Classes/Providers/OTT/Services/OTTUserService.swift @@ -11,57 +11,32 @@ import SwiftyJSON public class OTTUserService: NSObject { - internal static func login(baseURL: String, partnerId: Int64, username: String, password: String, udid: String? = nil) -> KalturaRequestBuilder? { - + static func login(baseURL: String, partnerId: Int64, username: String, password: String) -> 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? { + + static func refreshSession(baseURL: String, refreshToken: String, ks: String) -> 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? { + + static func anonymousLogin(baseURL: String, partnerId: Int64) -> 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/Services/PhoenixAPIDefines.swift b/Classes/Providers/OTT/Services/PhoenixAPIDefines.swift deleted file mode 100644 index 1257d623..00000000 --- a/Classes/Providers/OTT/Services/PhoenixAPIDefines.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// PhoenixAPIDefines.swift -// Pods -// -// Created by Rivka Peleg on 02/03/2017. -// -// - -import Foundation - -public enum AssetType: String { - case media = "media" - case epg = "epg" -} - -public enum PlaybackContextType: String { - - case trailer = "TRAILER" - case catchup = "CATCHUP" - case startOver = "START_OVER" - case playback = "PLAYBACK" -} diff --git a/Classes/Providers/OTT/Session/OTTSessionManager.swift b/Classes/Providers/OTT/Session/OTTSessionManager.swift index eaa5f26e..5549aff5 100644 --- a/Classes/Providers/OTT/Session/OTTSessionManager.swift +++ b/Classes/Providers/OTT/Session/OTTSessionManager.swift @@ -8,324 +8,57 @@ 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 + + enum SessionManagerError: Error{ + case failedToGetKS case failedToGetLoginResponse case failedToRefreshKS - case failedToLogout - } - - public weak var delegate: OTTSessionManagerDelegate? - public var saftyMargin: TimeInterval = 0 - + case failedToBuildRefreshRequest + case invalidRefreshCallResponse + case noRefreshTokenOrTokenToRefresh + case failedToParseResponse + } + + let saftyMargin = 5*60.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 - /************************************************************/ + + private var ks: String? + private var refreshToken: String? + private var tokenExpiration: Date? + public init(serverURL: String, partnerId: Int64, executor: RequestExecutor?) { self.serverURL = serverURL self.partnerId = partnerId - if let exe = executor { + 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 { - + + @objc public func startSession(username: String, password: String, completion: @escaping (_ error: Error?) -> Void) -> Void { + let loginRequestBuilder = OTTUserService.login(baseURL: self.serverURL, partnerId: partnerId, username: username, - password: password, - udid: udid) - + password: password) + 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 - + ks:"1:result:loginSession:ks") + + if let r1 = loginRequestBuilder, let r2 = sessionGetRequest { + + let mrb = KalturaMultiRequestBuilder(url: self.serverURL)?.add(request: r1).add(request: r2).setOTTBasicParams() + mrb?.set(completion: { (r:Response) in + if let data = r.data { var result: [OTTBaseObject]? = nil do { @@ -333,35 +66,130 @@ public struct SessionInfo { } catch { completion(error) } - - if let result = result, result.count == 2 { + + 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) + + if let loginObj = loginResult as? OTTLoginResponse, let sessionObj = sessionResult as? OTTSession { + + self.ks = loginObj.loginSession?.ks + self.refreshToken = loginObj.loginSession?.refreshToken + self.tokenExpiration = sessionObj.tokenExpiration + } - + completion(nil) } else { - completion(SessionManagerError.failed) + completion(SessionManagerError.failedToGetLoginResponse) } } else { - completion(SessionManagerError.failed) + completion(SessionManagerError.failedToGetLoginResponse) } }) - - let request = request.build() + + if let request = mrb?.build() { + self.executor.send(request: request) + } + } + } + + @objc public func startAnonymousSession(completion:@escaping (_ error: Error?) -> Void) { + + let loginRequestBuilder = OTTUserService.anonymousLogin(baseURL: self.serverURL, + partnerId: self.partnerId) + let sessionGetRequest = OTTSessionService.get(baseURL: self.serverURL, ks: "1:result:ks") + + if let r1 = loginRequestBuilder, let r2 = sessionGetRequest { + + let mrb = KalturaMultiRequestBuilder(url: self.serverURL)?.add(request: r1) + .setOTTBasicParams() + .add(request: r2).setOTTBasicParams() + .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 loginSession = result[0] as? OTTLoginSession, let session = result[1] as? OTTSession{ + + self.ks = loginSession.ks + self.refreshToken = loginSession.refreshToken + self.tokenExpiration = session.tokenExpiration + completion(nil) + } else { + completion(SessionManagerError.failedToGetLoginResponse) + } + } else { + completion(SessionManagerError.failedToGetLoginResponse) + } + }) + + if let request = mrb?.build() { + self.executor.send(request: request) + } + } + } + + @objc public func loadKS(completion: @escaping (String?, Error?) -> Void) { + let now = Date() + + if let expiration = self.tokenExpiration, expiration.timeIntervalSince(now) > saftyMargin { + completion(self.ks, nil) + } else { + self.refreshKS(completion: completion) + } + } + + @objc public func refreshKS(completion: @escaping (String?, Error?) -> Void) { + + guard let refreshToken = self.refreshToken, let ks = self.ks else { + completion(nil, SessionManagerError.noRefreshTokenOrTokenToRefresh) + return + } + + let refreshSessionRequest = OTTUserService.refreshSession(baseURL: self.serverURL, refreshToken: refreshToken, ks: ks) + let getSessionRequest = OTTSessionService.get(baseURL: self.serverURL, ks: "1:result:ks") + + guard let req1 = refreshSessionRequest, let req2 = getSessionRequest else { + completion(nil, SessionManagerError.invalidRefreshCallResponse) + return + } + + let mrb: KalturaMultiRequestBuilder? = (KalturaMultiRequestBuilder(url: self.serverURL)?.add(request: req1).add(request: req2))?.setOTTBasicParams().set(completion: { (r:Response) in + + guard let data = r.data else { + completion(nil, SessionManagerError.failedToRefreshKS) + return + } + + var response: [OTTBaseObject]? = nil + do { + response = try OTTMultiResponseParser.parse(data: data) + } catch { + completion(nil, error) + } + + if let response = response, response.count == 2, let loginSession = response[0] as? OTTLoginSession, let session = response[1] as? OTTSession { + + self.ks = loginSession.ks + self.refreshToken = loginSession.refreshToken + self.tokenExpiration = session.tokenExpiration + completion(self.ks, nil) + } else { + completion(nil, SessionManagerError.failedToRefreshKS) + } + }) + + if let request = mrb?.build() { self.executor.send(request: request) + } else { + completion(nil, SessionManagerError.failedToBuildRefreshRequest) + } } - } + diff --git a/Classes/Providers/OVP/Model/OVPSource.swift b/Classes/Providers/OVP/Model/OVPSource.swift index 3218267c..2aaf0f3e 100644 --- a/Classes/Providers/OVP/Model/OVPSource.swift +++ b/Classes/Providers/OVP/Model/OVPSource.swift @@ -12,7 +12,7 @@ import SwiftyJSON class OVPSource: OVPBaseObject { var deliveryProfileId: Int64 - var format: String + var format: String? var protocols: [String]? var flavors: [String]? var url: URL? @@ -31,14 +31,12 @@ class OVPSource: OVPBaseObject { let jsonObject = JSON(json) - guard let id = jsonObject[deliveryProfileIdKey].int64, - let format = jsonObject[formatKey].string - else { - return nil + guard let id = jsonObject[deliveryProfileIdKey].int64 else { + return nil } self.deliveryProfileId = id - self.format = format + self.format = jsonObject[formatKey].string if let protocols = jsonObject[protocolsKey].string{ self.protocols = protocols.components(separatedBy: ",") } diff --git a/Classes/Providers/OVP/OVPMediaProvider.swift b/Classes/Providers/OVP/OVPMediaProvider.swift index 5cefa1b2..c8915f30 100644 --- a/Classes/Providers/OVP/OVPMediaProvider.swift +++ b/Classes/Providers/OVP/OVPMediaProvider.swift @@ -184,8 +184,7 @@ import SwiftyXMLParser var mediaSources: [MediaSource] = [MediaSource]() sources.forEach { (source: OVPSource) in //detecting the source type - - let format = FormatsHelper.getMediaFormat(format: source.format, hasDrm: source.drm != nil) + let format = self.getSourceFormat(source: source) //If source type is not supported source will not be created guard format != .unknown else { return } @@ -256,8 +255,26 @@ import SwiftyXMLParser } - - + // This method decding the source type base on scheck and drm data + private func getSourceFormat(source: OVPSource) -> MediaSource.MediaFormat { + + if let format = source.format { + switch format { + case "applehttp": + return .hls + case "url": + if source.drm == nil { + return .mp4 + } else { + return .wvm + } + default: + return .unknown + } + } + + return .unknown + } // Creating the drm data based on scheme private func buildDRMParams(drm: [OVPDRM]?) -> [DRMParams]? { @@ -289,7 +306,7 @@ import SwiftyXMLParser // building the url with the SourceBuilder class private func playbackURL(loadInfo: LoaderInfo, source: OVPSource, ks: String?) -> URL? { - let formatType = FormatsHelper.getMediaFormat(format: source.format, hasDrm: source.drm != nil) + let formatType = self.getSourceFormat(source: source) var playURL: URL? = nil if let flavors = source.flavors, flavors.count > 0 { diff --git a/Example/PlayKit.xcodeproj/project.pbxproj b/Example/PlayKit.xcodeproj/project.pbxproj index b51da04c..f40b0675 100644 --- a/Example/PlayKit.xcodeproj/project.pbxproj +++ b/Example/PlayKit.xcodeproj/project.pbxproj @@ -22,14 +22,14 @@ 8460889C1DD1AB1A009E0E7A /* Entries.json in Resources */ = {isa = PBXBuildFile; fileRef = 8460889B1DD1AB1A009E0E7A /* Entries.json */; }; BF5C80821DF0E36500D3E665 /* MessegeBusTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5C80811DF0E36500D3E665 /* MessegeBusTest.swift */; }; C23A62D11DF413FF00635FA2 /* MockMediaProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62C91DF412FD00635FA2 /* MockMediaProviderTest.swift */; }; - C23A62D21DF4140B00635FA2 /* PhoenixMediaProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62CA1DF412FD00635FA2 /* PhoenixMediaProviderTest.swift */; }; + C23A62D21DF4140B00635FA2 /* OTTMediaProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62CA1DF412FD00635FA2 /* OTTMediaProviderTest.swift */; }; + C23A62D31DF4140E00635FA2 /* OVPMediaProviederTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62CB1DF412FD00635FA2 /* OVPMediaProviederTest.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 */; }; - C2D803221E6C09CF00A3DE15 /* SourceSelectorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB09C99B1E28072900D3671F /* SourceSelectorTest.swift */; }; C63FA2B01DF3F854004030E0 /* PlayerControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63FA2AE1DF3F854004030E0 /* PlayerControllerTest.swift */; }; + FB09C99C1E28072900D3671F /* SourceSelectorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB09C99B1E28072900D3671F /* SourceSelectorTest.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -69,7 +69,7 @@ BE76A2A9EC2DDAC016A40200 /* Pods_PlayKit_PlayKit_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PlayKit_PlayKit_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BF5C80811DF0E36500D3E665 /* MessegeBusTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessegeBusTest.swift; sourceTree = ""; }; C23A62C91DF412FD00635FA2 /* MockMediaProviderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMediaProviderTest.swift; sourceTree = ""; }; - C23A62CA1DF412FD00635FA2 /* PhoenixMediaProviderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhoenixMediaProviderTest.swift; sourceTree = ""; }; + C23A62CA1DF412FD00635FA2 /* OTTMediaProviderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTTMediaProviderTest.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 = ""; }; @@ -242,7 +242,7 @@ children = ( C23A62DB1DF47D9800635FA2 /* OTTSessionProviderTest.swift */, C23A62C91DF412FD00635FA2 /* MockMediaProviderTest.swift */, - C23A62CA1DF412FD00635FA2 /* PhoenixMediaProviderTest.swift */, + C23A62CA1DF412FD00635FA2 /* OTTMediaProviderTest.swift */, C23A62CB1DF412FD00635FA2 /* OVPMediaProviederTest.swift */, C23A62CF1DF4131700635FA2 /* MediaEntryProviderMock.swift */, ); @@ -476,14 +476,14 @@ files = ( C23A62D41DF4141000635FA2 /* MediaEntryProviderMock.swift in Sources */, C23A62D11DF413FF00635FA2 /* MockMediaProviderTest.swift in Sources */, - C2D803211E6C091600A3DE15 /* OVPMediaProviederTest.swift in Sources */, + C23A62D31DF4140E00635FA2 /* OVPMediaProviederTest.swift in Sources */, 20CD64591E4C9697002C9401 /* PluginTestConfiguration.swift in Sources */, C63FA2B01DF3F854004030E0 /* PlayerControllerTest.swift in Sources */, - C2D803221E6C09CF00A3DE15 /* SourceSelectorTest.swift in Sources */, 2012AFB21E4B860B00BBA61C /* PhoenixPluginTest.swift in Sources */, BF5C80821DF0E36500D3E665 /* MessegeBusTest.swift in Sources */, 2012AFB41E4B872300BBA61C /* PlayerCreator.swift in Sources */, - C23A62D21DF4140B00635FA2 /* PhoenixMediaProviderTest.swift in Sources */, + FB09C99C1E28072900D3671F /* SourceSelectorTest.swift in Sources */, + C23A62D21DF4140B00635FA2 /* OTTMediaProviderTest.swift in Sources */, 2012AFAF1E4B85CA00BBA61C /* OTTAnalyticsPluginTest.swift in Sources */, C23A62DD1DF47D9C00635FA2 /* OTTSessionProviderTest.swift in Sources */, ); diff --git a/Example/Podfile.lock b/Example/Podfile.lock index bd46cf89..a02d6c7c 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -67,6 +67,6 @@ SPEC CHECKSUMS: Youbora-AVPlayer: 02aea2a12a4f7e6a61d8a1747e5dfc177bf2354b Youbora-YouboraLib: 523adf7cd09c4a213e3485e6ec7c48d498986246 -PODFILE CHECKSUM: 8702b84cf4a02b3d2de2ccf0fe915659f256d0e6 +PODFILE CHECKSUM: 25eed3c6d61ea6aa08f2373344715de01155899a COCOAPODS: 1.2.0.beta.1 diff --git a/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift b/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift index 74de74a3..d2ce1d5d 100644 --- a/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift +++ b/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift @@ -42,8 +42,9 @@ class MockMediaProviderTest: XCTestCase { .set(url: self.filePath!) .set(id: "m001") - mediaProvider1.loadMedia { (media, error) in - if media != nil { + mediaProvider1.loadMedia { (r:Result) in + print(r) + if r.data != nil { theExeption.fulfill() } else{ @@ -63,8 +64,8 @@ class MockMediaProviderTest: XCTestCase { let theExeption = expectation(description: "test") let mediaProvider2 : MediaEntryProvider = MockMediaEntryProvider().set(url: self.filePath!).set(id: "sdf") - mediaProvider2.loadMedia { (media, error) in - if error != nil { + mediaProvider2.loadMedia { (r:Result) in + if r.error != nil { theExeption.fulfill() }else{ XCTFail() @@ -83,8 +84,8 @@ class MockMediaProviderTest: XCTestCase { let theExeption = expectation(description: "test") let mediaProvider2 : MediaEntryProvider = MockMediaEntryProvider().set(url: URL(string:"asdd")).set(id: "sdf") - mediaProvider2.loadMedia { (media, error) in - if error != nil { + mediaProvider2.loadMedia { (r:Result) in + if r.error != nil { theExeption.fulfill() }else{ XCTFail() @@ -105,8 +106,8 @@ class MockMediaProviderTest: XCTestCase { .set(content: self.fileContent) .set(id: "m001") - mediaProvider1.loadMedia { (media, error) in - if let mediaEntry = media { + mediaProvider1.loadMedia { (r:Result) in + if let mediaEntry = r.data { if let sources = mediaEntry.sources, sources.count > 0{ let source = sources[0] diff --git a/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift b/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift new file mode 100644 index 00000000..cd0a3c77 --- /dev/null +++ b/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift @@ -0,0 +1,60 @@ +// +// OTTMediaProviderTest.swift +// PlayKit +// +// Created by Rivka Peleg on 04/12/2016. +// Copyright © 2016 CocoaPods. All rights reserved. +// + +import XCTest +import PlayKit + + + +class OTTMediaProviderTest: XCTestCase, SessionProvider { + + + + + let mediaID = "258656" + var partnerId: Int64 = 198 + var serverURL: String = "http://52.210.223.65:8080/v4_0/api_v3" + + public func loadKS(completion: @escaping (Result) -> Void) { + completion(Result(data: "djJ8MTk4fLsl2jWZfTLBHh80n32POkgauZLWcLXhEEySDRL9yRtOLtr92sPWaKpnCaz4nJgsjjXIxD6PkOLXlOvpEHV3Wizc384sF3F4Kj1MfiqJRQd8", error: nil)) + } + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testRegularCaseTest() { + + let theExeption = expectation(description: "test") + + let provider = OTTMediaProvider() + .set(sessionProvider: self) + .set(mediaId: mediaID) + .set(type: AssetType.media) + .set(formats: ["Mobile_Devices_Main_HD"]) + + provider.loadMedia { (r:Result) in + print(r) + if (r.error != nil){ + theExeption.fulfill() + }else{ + XCTFail() + } + } + + self.waitForExpectations(timeout: 6.0) { (_) -> Void in + + } + } +} + + diff --git a/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift b/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift index 82c44ae7..e305e734 100644 --- a/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift +++ b/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift @@ -21,11 +21,12 @@ class OTTSessionProviderTest: XCTestCase { func testOTTSessionProvider() { - let sessionProvider = OTTSessionManager(serverURL:"http://52.210.223.65:8080/v4_2/api_v3", partnerId:198, executor: nil) + let sessionProvider = OTTSessionManager(serverURL:"http://52.210.223.65:8080/v4_0/api_v3", partnerId:198, executor: nil) sessionProvider.startAnonymousSession { (e:Error?) in if e == nil{ - sessionProvider.loadKS(completion: { (ks, error) in - print(ks ?? "") + sessionProvider.loadKS(completion: { (r:Result) in + print(r.data) + }) }else{ diff --git a/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift b/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift index 85724b26..2f0f0be2 100644 --- a/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift +++ b/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift @@ -14,8 +14,8 @@ import PlayKit class OVPMediaProviederTest: XCTestCase, SessionProvider { - public func loadKS(completion: @escaping (String?, Error?) -> Void) { - completion("", nil) + public func loadKS(completion: @escaping (Result) -> Void) { + completion(Result(data: "djJ8MjIyMjQwMXwdcXO1uXvBNZYxpUCxIGfEN120AWUJGJYCTt2qbhE3hCXa62-TGAOrxUtA0WwBGCqRreBaAzd2Dnejy9bYmcqtC1SxtCkZjw_jwoFd4Y3Cl-9hYgSCTcLRdqiePConBm8=", error: nil)) } let entryID = "1_ytsd86sc" @@ -40,13 +40,13 @@ class OVPMediaProviederTest: XCTestCase, SessionProvider { .set(entryId: self.entryID) .set(executor: MediaEntryProviderMockExecutor(entryID: entryID, domain: "ovp")) - provider.loadMedia { (media, error) in - if (error != nil){ + provider.loadMedia { (r:Result) in + if (r.error != nil){ XCTFail() }else{ theExeption.fulfill() } - + print(r) } @@ -65,12 +65,13 @@ class OVPMediaProviederTest: XCTestCase, SessionProvider { .set(executor: USRExecutor.shared ) - provider.loadMedia { (media, error) in - if (error != nil){ + provider.loadMedia { (r:Result) in + if (r.error != nil){ XCTFail() }else{ theExeption.fulfill() } + print(r) } diff --git a/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift b/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift deleted file mode 100644 index 6c3252b4..00000000 --- a/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// PhoenixMediaProviderTest.swift -// PlayKit -// -// Created by Rivka Peleg on 04/12/2016. -// Copyright © 2016 CocoaPods. All rights reserved. -// - -import XCTest -import PlayKit - - - -class PhoenixMediaProviderTest: XCTestCase, SessionProvider { - - - - - let mediaID = "485293" - var partnerId: Int64 = 198 - var serverURL: String = "http://api-preprod.ott.kaltura.com/v4_2/api_v3" - - - public func loadKS(completion: @escaping (String?, Error?) -> Void) { - completion(nil, nil) - } - - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - - func testRegularCaseTest() { - - let theExeption = expectation(description: "test") - - let provider = PhoenixMediaProvider() - .set(sessionProvider: self) - .set(assetId: mediaID) - .set(type: AssetType.media) - .set(playbackContextType: PlaybackContextType.playback) - - - provider.loadMedia { (entry, error) in - print(entry ?? "") - theExeption.fulfill() - } - - self.waitForExpectations(timeout: 6.0) { (_) -> Void in - - } - } -} - - diff --git a/Example/Tests/MessegeBusTest.swift b/Example/Tests/MessegeBusTest.swift index 8e4ffe4e..ef9bf06e 100644 --- a/Example/Tests/MessegeBusTest.swift +++ b/Example/Tests/MessegeBusTest.swift @@ -29,9 +29,7 @@ class MessegeBusTest: XCTestCase { entry["sources"] = sources let mediaConfig = MediaConfig(mediaEntry: MediaEntry(json: entry)) - do{ - self.player = try PlayKitManager.shared.loadPlayer(pluginConfig: nil) - }catch{} + self.player = PlayKitManager.shared.loadPlayer(pluginConfig: nil) self.player.prepare(mediaConfig) } diff --git a/Example/Tests/PlayerControllerTest.swift b/Example/Tests/PlayerControllerTest.swift index 8b55b960..065409c2 100644 --- a/Example/Tests/PlayerControllerTest.swift +++ b/Example/Tests/PlayerControllerTest.swift @@ -32,11 +32,7 @@ class PlayerControllerTest: XCTestCase { entry["sources"] = sources let mediaConfig = MediaConfig(mediaEntry: MediaEntry(json: entry)) - do{ - self.player = try PlayKitManager.shared.loadPlayer(pluginConfig: nil) - } catch { - - } + self.player = PlayKitManager.shared.loadPlayer(pluginConfig: nil) self.player.prepare(mediaConfig) } diff --git a/Example/Tests/PlayerCreator.swift b/Example/Tests/PlayerCreator.swift index b0ffa10c..8183f720 100644 --- a/Example/Tests/PlayerCreator.swift +++ b/Example/Tests/PlayerCreator.swift @@ -41,28 +41,20 @@ extension PlayerCreator where Self: QuickSpec { entry["id"] = "test" entry["sources"] = sources let mediaConfig = MediaConfig(mediaEntry: MediaEntry(json: entry)) - do{ + let pluginConfig: PluginConfig? if let pluginConfigDict = pluginConfigDict { let pluginConfig = PluginConfig(config: pluginConfigDict) - - - player = try PlayKitManager.shared.loadPlayer(pluginConfig: pluginConfig) as! PlayerLoader + player = PlayKitManager.shared.loadPlayer(pluginConfig: pluginConfig) as! PlayerLoader } else { pluginConfig = nil - player = try PlayKitManager.shared.loadPlayer(pluginConfig: pluginConfig) as! PlayerLoader - } - - if shouldStartPreparing { - player.prepare(mediaConfig) - } - return player - }catch{ - + player = PlayKitManager.shared.loadPlayer(pluginConfig: pluginConfig) as! PlayerLoader } - return PlayerLoader() - + if shouldStartPreparing { + player.prepare(mediaConfig) + } + return player } } From b010c7dfe7eff03b7701f4bfdcbccb3def0b7e44 Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Tue, 28 Mar 2017 15:02:38 +0300 Subject: [PATCH 05/27] #FEM-1285 (#119) * #FEM-1285 * Removed ad tag url manual ad list management. * Renamed adAllCompleted to allAdsCompleted (it is a type to call the event adAllCompleted, original event is call All Ads Completed) * Merge branch 'develop' into FEM-1285 * Small fix to destroy ads manager --- Classes/PlayerEvent.swift | 6 +-- Plugins/IMA/AdsConfig.swift | 7 ---- Plugins/IMA/IMAPlugin.swift | 75 +++++-------------------------------- 3 files changed, 12 insertions(+), 76 deletions(-) diff --git a/Classes/PlayerEvent.swift b/Classes/PlayerEvent.swift index de818949..22f71c45 100644 --- a/Classes/PlayerEvent.swift +++ b/Classes/PlayerEvent.swift @@ -121,13 +121,13 @@ import AVFoundation @objc public class AdEvent: PKEvent { @objc public static let allEventTypes: [AdEvent.Type] = [ - adBreakReady, adBreakEnded, adBreakStarted, adAllCompleted, adComplete, adClicked, adCuePointsUpdate, adFirstQuartile, adLoaded, adLog, adMidpoint, adPaused, adResumed, adSkipped, adStarted, adStreamLoaded, adTapped, adThirdQuartile, adDidProgressToTime, adDidRequestPause, adDidRequestResume, adWebOpenerWillOpenExternalBrowser, adWebOpenerWillOpenInAppBrowser, adWebOpenerDidOpenInAppBrowser, adWebOpenerWillCloseInAppBrowser, adWebOpenerDidCloseInAppBrowser + 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 ] @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 adAllCompleted: AdEvent.Type = AdAllCompleted.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 @objc public static let adFirstQuartile: AdEvent.Type = AdFirstQuartile.self @@ -158,7 +158,7 @@ import AVFoundation class AdBreakReady: AdEvent {} class AdBreakEnded: AdEvent {} class AdBreakStarted: AdEvent {} - class AdAllCompleted: AdEvent {} + class AllAdsCompleted: AdEvent {} class AdComplete: AdEvent {} class AdClicked: AdEvent {} class AdFirstQuartile: AdEvent {} diff --git a/Plugins/IMA/AdsConfig.swift b/Plugins/IMA/AdsConfig.swift index 54178bfa..0950e558 100644 --- a/Plugins/IMA/AdsConfig.swift +++ b/Plugins/IMA/AdsConfig.swift @@ -20,7 +20,6 @@ import GoogleInteractiveMediaAds @objc public var videoBitrate = kIMAAutodetectBitrate @objc public var videoMimeTypes: [Any]? @objc public var adTagUrl: String? - @objc public var tagsTimes: [TimeInterval: String]? @objc public var companionView: UIView? @objc public var webOpenerPresentingController: UIViewController? @@ -49,12 +48,6 @@ import GoogleInteractiveMediaAds return self } - @discardableResult - @nonobjc public func set(tagsTimes: [TimeInterval: String]) -> Self { - self.tagsTimes = tagsTimes - return self - } - @discardableResult @nonobjc public func set(companionView: UIView) -> Self { self.companionView = companionView diff --git a/Plugins/IMA/IMAPlugin.swift b/Plugins/IMA/IMAPlugin.swift index 43b8c4dc..5b5d9ca3 100644 --- a/Plugins/IMA/IMAPlugin.swift +++ b/Plugins/IMA/IMAPlugin.swift @@ -9,7 +9,6 @@ import GoogleInteractiveMediaAds extension IMAAdsManager { - func getAdCuePoints() -> PKAdCuePoints { return PKAdCuePoints(cuePoints: self.adCuePoints as? [TimeInterval] ?? []) } @@ -25,7 +24,6 @@ extension IMAAdsManager { weak var delegate: AdsPluginDelegate? weak var pipDelegate: AVPictureInPictureControllerDelegate? - private var contentPlayhead: IMAAVPlayerContentPlayhead? private var adsManager: IMAAdsManager? private var renderingSettings: IMAAdsRenderingSettings! = IMAAdsRenderingSettings() private static var loader: IMAAdsLoader! @@ -34,23 +32,14 @@ extension IMAAdsManager { private var loadingView: UIView? // we must have config error will be thrown otherwise private var config: AdsConfig! - private var adTagUrl: String? - private var tagsTimes: [TimeInterval : String]? { - didSet { - sortedTagsTimes = tagsTimes!.keys.sorted() - } - } - private var sortedTagsTimes: [TimeInterval]? + private var adTagUrl: String! - private var currentPlaybackTime: TimeInterval! = 0 //in seconds private var isAdPlayback = false private var startAdCalled = false private var loaderFailed = false - private var timer: Timer? - public var currentTime: TimeInterval { - return self.currentPlaybackTime + return self.player?.currentTime ?? 0 } /************************************************************/ @@ -83,9 +72,6 @@ extension IMAAdsManager { if let adTagUrl = adsConfig.adTagUrl { self.adTagUrl = adTagUrl - } else if let adTagsTimes = adsConfig.tagsTimes { - self.tagsTimes = adTagsTimes - self.sortedTagsTimes = adTagsTimes.keys.sorted() } } else { PKLog.error("missing plugin config") @@ -95,14 +81,11 @@ extension IMAAdsManager { self.messageBus?.addObserver(self, events: [PlayerEvent.ended]) { [weak self] event in self?.contentComplete() } - - self.timer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(IMAPlugin.update), userInfo: nil, repeats: true) } public override func destroy() { super.destroy() self.destroyManager() - self.timer?.invalidate() } /************************************************************/ @@ -210,49 +193,6 @@ extension IMAAdsManager { videoView.addConstraint(NSLayoutConstraint(item: videoView, attribute: NSLayoutAttribute.right, relatedBy: NSLayoutRelation.equal, toItem: self.loadingView!, attribute: NSLayoutAttribute.right, multiplier: 1, constant: 0)) } } - - private func loadAdsIfNeeded() { - if self.tagsTimes != nil { - let key = floor(self.currentPlaybackTime) - if self.sortedTagsTimes!.count > 0 && key >= self.sortedTagsTimes![0] { - if let adTag = self.tagsTimes![key] { - self.updateAdTag(adTag, tagTimeKeyForRemove: key) - } else { - let closestKey = self.findClosestTimeInterval(for: key) - let adTag = self.tagsTimes![closestKey] - self.updateAdTag(adTag!, tagTimeKeyForRemove: closestKey) - } - } - } - } - - private func updateAdTag(_ adTag: String, tagTimeKeyForRemove: TimeInterval) { - self.tagsTimes![tagTimeKeyForRemove] = nil - self.destroyManager() - self.contentComplete() - self.adTagUrl = adTag - self.requestAds() - self.start(showLoadingView: false) - } - - private func findClosestTimeInterval(for searchItem: TimeInterval) -> TimeInterval { - var result = self.sortedTagsTimes![0] - for item in self.sortedTagsTimes! { - if item > searchItem { - break - } - result = item - } - return result - } - - @objc private func update() { - if !self.isAdPlayback { - guard let currentTime = self.player?.currentTime, !currentTime.isNaN else { return } - self.currentPlaybackTime = currentTime - self.loadAdsIfNeeded() - } - } private func createRenderingSettings() { self.renderingSettings.webOpenerDelegate = self @@ -292,6 +232,7 @@ extension IMAAdsManager { } private func destroyManager() { + self.adsManager?.delegate = nil self.adsManager?.destroy() self.adsManager = nil } @@ -337,6 +278,7 @@ extension IMAAdsManager { 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) @@ -367,7 +309,11 @@ extension IMAAdsManager { self.notify(event: AdEvent.AdBreakStarted()) self.showLoadingView(false, alpha: 0) case .AD_BREAK_ENDED: self.notify(event: AdEvent.AdBreakEnded()) - case .ALL_ADS_COMPLETED: self.notify(event: AdEvent.AdAllCompleted()) + 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())) @@ -402,9 +348,6 @@ extension IMAAdsManager { } public func adsManager(_ adsManager: IMAAdsManager!, adDidProgressToTime mediaTime: TimeInterval, totalTime: TimeInterval) { - var data = [String: TimeInterval]() - data["mediaTime"] = mediaTime - data["totalTime"] = totalTime self.notify(event: AdEvent.AdDidProgressToTime(mediaTime: mediaTime, totalTime: totalTime)) } From 6963e374f20d14269537d21f9c48373a0c18456d Mon Sep 17 00:00:00 2001 From: srivkas Date: Tue, 28 Mar 2017 16:41:45 +0300 Subject: [PATCH 06/27] Fem 1165 (#124) * Renaming ott media provider to ovp media provider * Update tests to the latest code * Adding first implementation for Phoenix media provider with playback context * fixing compilation issue * Fixing the test and changing the tested asset * Fixing object type issue * deleting unnecessary enum * Adding condition for not supported format and schemes * Adding udid to OTT session * Adding social service ( in order to login via Facebook ) * adding did to login and refresh api's * Adding recovery session method adding udid adding '{}' to forward parameter * Error handling * set recovery params as optional set token expiration getter public * Adding logout and creating class for session info * Fixing asset builder DRMSupport checker * fixing Facebook login request builder * removing logout from session when refresh is failed * Adding documentation for phoenix media provider * delete unnecessary errors * Adding switch profile API unifying all session requests into one * changing safety margin to be mutable * Fixing codacy issues * Fixing codacy issues * swiftlint Classes/Providers/OTT. * More swiftlint. * Objective c compatibility and other syntax fixing. --- Classes/PKError.swift | 2 +- Classes/Player/AssetBuilder.swift | 4 +- Classes/Player/MediaEntry.swift | 5 +- Classes/Providers/Base/FormatsHelper.swift | 32 ++ Classes/Providers/MediaEntryProvider.swift | 10 +- .../Providers/Mock/MockMediaProvider.swift | 36 +- Classes/Providers/OTT/Model/OTTAsset.swift | 19 +- .../Providers/OTT/Model/OTTBaseObject.swift | 4 +- Classes/Providers/OTT/Model/OTTDrmData.swift | 31 ++ Classes/Providers/OTT/Model/OTTError.swift | 22 +- Classes/Providers/OTT/Model/OTTFile.swift | 24 +- .../OTT/Model/OTTGetAssetResponse.swift | 9 +- .../Providers/OTT/Model/OTTLicensedURL.swift | 11 +- .../OTT/Model/OTTLoginResponse.swift | 10 +- .../Providers/OTT/Model/OTTLoginSession.swift | 8 +- .../OTT/Model/OTTPlaybackContext.swift | 24 + .../OTT/Model/OTTPlaybackSource.swift | 58 +++ .../OTT/Model/OTTRefreshedSession.swift | 10 +- Classes/Providers/OTT/Model/OTTSession.swift | 11 +- Classes/Providers/OTT/OTTMediaProvider.swift | 156 ------ .../OTT/Parsers/OTTMultiResponseParser.swift | 18 +- .../OTT/Parsers/OTTObjectMapper.swift | 12 +- .../OTT/Parsers/OTTResponseParser.swift | 11 +- .../Providers/OTT/PhoenixMediaProvider.swift | 393 +++++++++++++++ .../OTT/Services/OTTAssetService.swift | 52 +- .../OTT/Services/OTTLicensedURLService.swift | 9 +- .../OTT/Services/OTTSessionService.swift | 22 +- .../OTT/Services/OTTSocialService.swift | 41 ++ .../OTT/Services/OTTUserService.swift | 39 +- .../OTT/Services/PhoenixAPIDefines.swift | 44 ++ .../OTT/Session/OTTSessionManager.swift | 464 ++++++++++++------ Classes/Providers/OVP/Model/OVPSource.swift | 10 +- Classes/Providers/OVP/OVPMediaProvider.swift | 27 +- Example/PlayKit.xcodeproj/project.pbxproj | 16 +- Example/Podfile.lock | 2 +- .../Analytics/OTTAnalyticsPluginTest.swift | 8 +- .../MockMediaProviderTest.swift | 17 +- .../OTTMediaProviderTest.swift | 60 --- .../OTTSessionProviderTest.swift | 7 +- .../OVPMediaProviederTest.swift | 15 +- .../PhoenixMediaProviderTest.swift | 58 +++ Example/Tests/MessegeBusTest.swift | 4 +- Example/Tests/PlayerControllerTest.swift | 6 +- Example/Tests/PlayerCreator.swift | 22 +- 44 files changed, 1254 insertions(+), 589 deletions(-) create mode 100644 Classes/Providers/Base/FormatsHelper.swift create mode 100644 Classes/Providers/OTT/Model/OTTDrmData.swift create mode 100644 Classes/Providers/OTT/Model/OTTPlaybackContext.swift create mode 100644 Classes/Providers/OTT/Model/OTTPlaybackSource.swift delete mode 100644 Classes/Providers/OTT/OTTMediaProvider.swift create mode 100644 Classes/Providers/OTT/PhoenixMediaProvider.swift create mode 100644 Classes/Providers/OTT/Services/OTTSocialService.swift create mode 100644 Classes/Providers/OTT/Services/PhoenixAPIDefines.swift delete mode 100644 Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift create mode 100644 Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift diff --git a/Classes/PKError.swift b/Classes/PKError.swift index ebcb963f..3e3cb0e5 100644 --- a/Classes/PKError.swift +++ b/Classes/PKError.swift @@ -160,7 +160,7 @@ protocol PKError: Error, CustomStringConvertible { extension PKError { /// description string - var description: String { + public var description: String { return "\(type(of: self)) ,domain: \(type(of: self).domain), errorCode: \(self.code)" } diff --git a/Classes/Player/AssetBuilder.swift b/Classes/Player/AssetBuilder.swift index c8740a6e..928974d9 100644 --- a/Classes/Player/AssetBuilder.swift +++ b/Classes/Player/AssetBuilder.swift @@ -99,7 +99,7 @@ enum AssetError : Error { class DRMSupport { // FairPlay is not available in simulators and before iOS8 static let fairplay: Bool = { - if Platform.isSimulator, #available(iOS 8, *) { + if !Platform.isSimulator, #available(iOS 8, *) { return true } else { return false @@ -108,7 +108,7 @@ class DRMSupport { // FairPlay is not available in simulators and is only downloadable in iOS10 and up. static let fairplayOffline: Bool = { - if Platform.isSimulator, #available(iOS 10, *) { + if !Platform.isSimulator, #available(iOS 10, *) { return true } else { return false diff --git a/Classes/Player/MediaEntry.swift b/Classes/Player/MediaEntry.swift index 86b8b30b..958fb477 100644 --- a/Classes/Player/MediaEntry.swift +++ b/Classes/Player/MediaEntry.swift @@ -15,6 +15,7 @@ func getJson(_ json: Any) -> JSON { @objc public enum MediaType: Int { case live + case vod case unknown } @@ -136,7 +137,6 @@ func getJson(_ json: Any) -> JSON { private let idKey: String = "id" private let contentUrlKey: String = "url" - private let mimeTypeKey: String = "mimeType" private let drmDataKey: String = "drmData" private let formatTypeKey: String = "sourceType" @@ -147,7 +147,6 @@ func getJson(_ json: Any) -> JSON { @objc public init(_ id: String, contentUrl: URL?, mimeType: String? = nil, drmData: [DRMParams]? = nil, mediaFormat: MediaFormat = .unknown) { self.id = id self.contentUrl = contentUrl - self.mimeType = mimeType self.drmData = drmData self.mediaFormat = mediaFormat } @@ -160,8 +159,6 @@ func getJson(_ json: Any) -> JSON { self.contentUrl = sj[contentUrlKey].url - self.mimeType = sj[mimeTypeKey].string - if let drmData = sj[drmDataKey].array { self.drmData = drmData.flatMap { DRMParams.fromJSON($0) } } diff --git a/Classes/Providers/Base/FormatsHelper.swift b/Classes/Providers/Base/FormatsHelper.swift new file mode 100644 index 00000000..6e4d29e7 --- /dev/null +++ b/Classes/Providers/Base/FormatsHelper.swift @@ -0,0 +1,32 @@ +// +// FormatsHelper.swift +// Pods +// +// Created by Rivka Peleg on 05/03/2017. +// +// + +import Foundation + +class FormatsHelper { + + static let supportedFormats: [MediaSource.MediaFormat] = [.hls, .mp4, .wvm, .mp3] + static let supportedSchemes: [DRMParams.Scheme] = [.fairplay, .widevineClassic] + + static func getMediaFormat (format: String, hasDrm: Bool) -> MediaSource.MediaFormat { + + switch format { + case "applehttp": + return .hls + case "url": + if hasDrm { + return .wvm + } else { + return .mp4 + } + default: + return .unknown + } + } + +} diff --git a/Classes/Providers/MediaEntryProvider.swift b/Classes/Providers/MediaEntryProvider.swift index 48fcb97d..fe20b569 100644 --- a/Classes/Providers/MediaEntryProvider.swift +++ b/Classes/Providers/MediaEntryProvider.swift @@ -8,7 +8,6 @@ import UIKit - @objc public protocol MediaEntryProvider { /** This method is triggering the creation of media base on custom parameters and actions. @@ -32,12 +31,7 @@ import UIKit */ func loadMedia(callback: @escaping (MediaEntry?, Error?) -> Void) - - - func cancel() - -} - - + func cancel() +} diff --git a/Classes/Providers/Mock/MockMediaProvider.swift b/Classes/Providers/Mock/MockMediaProvider.swift index ebf40b7f..4a133e3c 100644 --- a/Classes/Providers/Mock/MockMediaProvider.swift +++ b/Classes/Providers/Mock/MockMediaProvider.swift @@ -10,52 +10,52 @@ import UIKit import SwiftyJSON @objc public class MockMediaEntryProvider: NSObject, MediaEntryProvider { - + public enum MockError: Error { case invalidParam(paramName:String) case fileIsEmptyOrNotFound case unableToParseJSON case mediaNotFound } - + @objc public var id: String? @objc public var url: URL? @objc public var content: Any? - + @discardableResult @nonobjc public func set(id: String?) -> Self { self.id = id return self } - + @discardableResult @nonobjc public func set(url: URL?) -> Self { self.url = url return self } - + @discardableResult @nonobjc public func set(content: Any?) -> Self { self.content = content return self } - - public override init(){ - + + public override init() { + } - + struct LoaderInfo { var id: String var content: JSON } @objc public func loadMedia(callback: @escaping (MediaEntry?, Error?) -> Void) { - + guard let id = self.id else { callback(nil, MockError.invalidParam(paramName: "id")) return } - + var json: JSON? = nil if let content = self.content { json = JSON(content) @@ -70,30 +70,30 @@ import SwiftyJSON } json = JSON(data: data as Data) } - + guard let jsonContent = json else { callback(nil, MockError.unableToParseJSON) return } - + let loderInfo = LoaderInfo(id: id, content: jsonContent) - + guard loderInfo.content != .null else { callback(nil, MockError.unableToParseJSON) return } - + let jsonObject: JSON = loderInfo.content[loderInfo.id] guard jsonObject != .null else { callback(nil, MockError.mediaNotFound) return } - + let mediaEntry = MediaEntry(json: jsonObject.object) callback(mediaEntry, nil) } - + public func cancel() { - + } } diff --git a/Classes/Providers/OTT/Model/OTTAsset.swift b/Classes/Providers/OTT/Model/OTTAsset.swift index af065b38..8f19ec63 100644 --- a/Classes/Providers/OTT/Model/OTTAsset.swift +++ b/Classes/Providers/OTT/Model/OTTAsset.swift @@ -11,29 +11,28 @@ import SwiftyJSON internal class OTTAsset: OTTBaseObject { - internal var id: String - internal var files: [OTTFile]? - + var id: String + var files: [OTTFile]? + private let idKey = "id" private let idfiles = "mediaFiles" - - internal required init?(json:Any) { - + + 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){ + if let file = OTTFile(json: jsonFile.object) { self.files?.append(file) } } } } } - diff --git a/Classes/Providers/OTT/Model/OTTBaseObject.swift b/Classes/Providers/OTT/Model/OTTBaseObject.swift index 654a285c..03bc15ac 100644 --- a/Classes/Providers/OTT/Model/OTTBaseObject.swift +++ b/Classes/Providers/OTT/Model/OTTBaseObject.swift @@ -9,8 +9,6 @@ import UIKit protocol OTTBaseObject { - + init?(json:Any) } - - diff --git a/Classes/Providers/OTT/Model/OTTDrmData.swift b/Classes/Providers/OTT/Model/OTTDrmData.swift new file mode 100644 index 00000000..9ef4d02a --- /dev/null +++ b/Classes/Providers/OTT/Model/OTTDrmData.swift @@ -0,0 +1,31 @@ +// +// OTTDrmPlaybackPluginData.swift +// Pods +// +// Created by Rivka Peleg on 05/03/2017. +// +// + +import Foundation +import SwiftyJSON + +class OTTDrmData: OTTBaseObject { + + var scheme: String + var licenseURL: String + var certificate: String? + + required init?(json: Any) { + let jsonObject = JSON(json) + + guard let scheme = jsonObject["scheme"].string, + let licenseURL = jsonObject["licenseURL"].string + else { + return nil + } + + self.scheme = scheme + self.licenseURL = licenseURL + self.certificate = jsonObject["certificate"].string + } +} diff --git a/Classes/Providers/OTT/Model/OTTError.swift b/Classes/Providers/OTT/Model/OTTError.swift index 6727f496..25905394 100644 --- a/Classes/Providers/OTT/Model/OTTError.swift +++ b/Classes/Providers/OTT/Model/OTTError.swift @@ -9,27 +9,21 @@ import UIKit import SwiftyJSON - - class OTTError: OTTBaseObject { - + var message: String? var code: String? - + let errorKey = "error" let messageKey = "message" let codeKey = "code" - - + required init?(json: Any) { - + let jsonObj: JSON = JSON(json) - self.message = jsonObj[errorKey][messageKey].string - self.code = jsonObj[errorKey][codeKey].string + let errorDict = jsonObj[errorKey] + self.message = errorDict[messageKey].string + self.code = errorDict[codeKey].string } - - init() { - - } - + } diff --git a/Classes/Providers/OTT/Model/OTTFile.swift b/Classes/Providers/OTT/Model/OTTFile.swift index 9ea06496..698836e7 100644 --- a/Classes/Providers/OTT/Model/OTTFile.swift +++ b/Classes/Providers/OTT/Model/OTTFile.swift @@ -10,33 +10,33 @@ import UIKit import SwiftyJSON internal class OTTFile: OTTBaseObject { - + internal var id: String - internal var type: String? = nil - internal var url: URL? = nil - internal var duration: TimeInterval? = nil - + 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){ + 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{ + } else { return nil } - + self.type = fileJosn[typeKey].string - if let contentURL = fileJosn[urlKey].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 index 7924d64d..36f8ee6d 100644 --- a/Classes/Providers/OTT/Model/OTTGetAssetResponse.swift +++ b/Classes/Providers/OTT/Model/OTTGetAssetResponse.swift @@ -11,15 +11,14 @@ import SwiftyJSON internal class OTTGetAssetResponse: OTTBaseObject { - internal var asset: OTTAsset? = nil - + 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 index 79f03709..ea40ec67 100644 --- a/Classes/Providers/OTT/Model/OTTLicensedURL.swift +++ b/Classes/Providers/OTT/Model/OTTLicensedURL.swift @@ -12,19 +12,18 @@ 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 index 0c3a1c10..263147e1 100644 --- a/Classes/Providers/OTT/Model/OTTLoginResponse.swift +++ b/Classes/Providers/OTT/Model/OTTLoginResponse.swift @@ -10,16 +10,16 @@ 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 index c1e652ef..4dc676b0 100644 --- a/Classes/Providers/OTT/Model/OTTLoginSession.swift +++ b/Classes/Providers/OTT/Model/OTTLoginSession.swift @@ -13,15 +13,15 @@ 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/OTTPlaybackContext.swift b/Classes/Providers/OTT/Model/OTTPlaybackContext.swift new file mode 100644 index 00000000..530e4557 --- /dev/null +++ b/Classes/Providers/OTT/Model/OTTPlaybackContext.swift @@ -0,0 +1,24 @@ +// +// OTTPlaybackContext.swift +// Pods +// +// Created by Rivka Peleg on 05/03/2017. +// +// + +import Foundation +import SwiftyJSON + +class OTTPlaybackContext: OTTBaseObject { + + var sources: [OTTPlaybackSource] = [] + + required init?(json: Any) { + let jsonObject = JSON(json) + jsonObject["sources"].array?.forEach { (source: JSON) in + if let source = OTTPlaybackSource(json: source.object) { + sources.append(source) + } + } + } +} diff --git a/Classes/Providers/OTT/Model/OTTPlaybackSource.swift b/Classes/Providers/OTT/Model/OTTPlaybackSource.swift new file mode 100644 index 00000000..e7f353f0 --- /dev/null +++ b/Classes/Providers/OTT/Model/OTTPlaybackSource.swift @@ -0,0 +1,58 @@ +// +// OTTPlaybackSource.swift +// Pods +// +// Created by Rivka Peleg on 05/03/2017. +// +// + +import Foundation +import SwiftyJSON + +class OTTPlaybackSource: OTTBaseObject { + + var assetId: Int + var id: Int + var type: String // file format + var url: URL? + var duration: Float + var externalId: String? + var protocols: [String] + var format: String + var drm: [OTTDrmData]? + + required init?(json: Any) { + let jsonObject = JSON(json) + + guard let assetId = jsonObject["assetId"].int, + let id = jsonObject["id"].int, + let type = jsonObject["type"].string, + let urlString = jsonObject["url"].string, + let protocolsString = jsonObject["protocols"].string, + let format = jsonObject["format"].string + else { + return nil + } + + self.assetId = assetId + self.id = id + self.type = type + self.url = URL.init(string: urlString) + self.protocols = protocolsString.components(separatedBy: ",") + self.format = format + self.duration = jsonObject["duration"].float ?? 0 + self.externalId = jsonObject["externalId"].string + + var drmArray = [OTTDrmData]() + jsonObject["drm"].array?.forEach {(json) in + if let drmObject = OTTDrmData(json: json.object) { + drmArray.append(drmObject) + } + } + + if drmArray.count > 0 { + self.drm = drmArray + } + + } +} diff --git a/Classes/Providers/OTT/Model/OTTRefreshedSession.swift b/Classes/Providers/OTT/Model/OTTRefreshedSession.swift index e61fb46b..eb143544 100644 --- a/Classes/Providers/OTT/Model/OTTRefreshedSession.swift +++ b/Classes/Providers/OTT/Model/OTTRefreshedSession.swift @@ -13,17 +13,15 @@ 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 index f6dd9716..60240116 100644 --- a/Classes/Providers/OTT/Model/OTTSession.swift +++ b/Classes/Providers/OTT/Model/OTTSession.swift @@ -9,18 +9,21 @@ 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{ + if let time = jsonObject[tokenExpirationKey].number?.doubleValue { self.tokenExpiration = Date.init(timeIntervalSince1970:time) } + self.udid = jsonObject[udidKey].string + } } diff --git a/Classes/Providers/OTT/OTTMediaProvider.swift b/Classes/Providers/OTT/OTTMediaProvider.swift deleted file mode 100644 index 70ec8f13..00000000 --- a/Classes/Providers/OTT/OTTMediaProvider.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// OTTEntryProvider.swift -// Pods -// -// Created by Admin on 13/11/2016. -// -// - -import UIKit -import SwiftyJSON - -@objc public class OTTMediaProvider: NSObject, MediaEntryProvider { - - public enum OTTMediaProviderError: Error { - case invalidInputParams - case invalidKS - case fileIsEmptyOrNotFound - case invalidJSON - case mediaNotFound - case currentlyProcessingOtherRequest - case unableToParseObject - } - - @objc public var sessionProvider: SessionProvider? - @objc public var mediaId: String? - @objc public var type: AssetType = .unknown - @objc public var formats: [String]? - public var executor: RequestExecutor? // TODO: make @objc if needed in the future - - public override init() {} - - @objc public init(_ sessionProvider: SessionProvider) { - self.sessionProvider = sessionProvider - } - - @discardableResult - @nonobjc public func set(sessionProvider: SessionProvider?) -> Self { - self.sessionProvider = sessionProvider - return self - } - - @discardableResult - @nonobjc public func set(mediaId:String?) -> Self { - self.mediaId = mediaId - return self - } - - @discardableResult - @nonobjc public func set(type: AssetType) -> Self { - self.type = type - return self - } - - @discardableResult - @nonobjc public func set(formats:[String]?) -> Self { - self.formats = formats - return self - } - - @discardableResult - @nonobjc public func set(executor:RequestExecutor?) -> Self { - self.executor = executor - return self - } - - struct LoaderInfo { - var sessionProvider: SessionProvider - var mediaId: String - var type: AssetType - var formats: [String] - var executor: RequestExecutor - } - - @objc public func loadMedia(callback: @escaping (MediaEntry?, Error?) -> Void) { - guard let sessionProvider = self.sessionProvider, - let mediaId = self.mediaId, - self.type != .unknown - else { - callback(nil, OTTMediaProviderError.invalidInputParams) - return - } - - var executor: RequestExecutor = USRExecutor.shared - var formats: [String] = [] - if let exe = self.executor{ - executor = exe - } - - if let fmts = self.formats { - formats = fmts - } - - let loaderParams = LoaderInfo(sessionProvider: sessionProvider, mediaId: mediaId, type: type, formats: formats, executor: executor) - self.startLoad(loader: loaderParams, callback: callback) - } - - public func cancel() { - - } - - func startLoad(loader: LoaderInfo, callback: @escaping (MediaEntry?, Error?) -> Void) { - loader.sessionProvider.loadKS { (ks, error) in - guard let ks = ks else { - callback(nil, OTTMediaProviderError.invalidKS) - return - } - - let requestBuilder = OTTAssetService.get(baseURL: loader.sessionProvider.serverURL, ks: ks, assetId: loader.mediaId, type:loader.type)? - .setOTTBasicParams() - .set(completion: { (r:Response) in - - guard let data = r.data else { - callback(nil, OTTMediaProviderError.mediaNotFound) - return - } - - var object: OTTBaseObject? = nil - do { - object = try OTTResponseParser.parse(data: data) - } catch { - callback(nil, error) - } - - if let asset = object as? OTTAsset { - - let mediaEntry: MediaEntry = MediaEntry(id: asset.id) - if let files = asset.files { - - var sources = [MediaSource]() - for file in files { - if let fileFormat = file.type{ - if loader.formats.contains(fileFormat) == true { - let source: MediaSource = MediaSource(id: file.id) - source.contentUrl = file.url - sources.append(source) - - } - } - } - - if sources.count > 0 { - mediaEntry.sources = sources - } - } - callback(mediaEntry, nil) - } else { - callback(nil, OTTMediaProviderError.mediaNotFound) - } - }) - if let assetRequest = requestBuilder?.build() { - loader.executor.send(request: assetRequest) - } - } - } -} - diff --git a/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift b/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift index f5195ea0..88822a76 100644 --- a/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift +++ b/Classes/Providers/OTT/Parsers/OTTMultiResponseParser.swift @@ -10,33 +10,33 @@ import UIKit import SwiftyJSON class OTTMultiResponseParser: NSObject { - + enum OTTMultiResponseParserError: Error { case typeNotFound case emptyResponse case notMultiResponse } - + static func parse(data:Any) throws -> [OTTBaseObject] { - + let jsonResponse = JSON(data) if let resultArrayJSON = jsonResponse["result"].array { - + var resultArray: [OTTBaseObject] = [OTTBaseObject]() - for jsonObject: JSON in resultArrayJSON{ + for jsonObject: JSON in resultArrayJSON { var object: OTTBaseObject? = nil let objectType: OTTBaseObject.Type? = OTTObjectMapper.classByJsonObject(json: jsonObject.dictionaryObject) - if let type = objectType{ + if let type = objectType { object = type.init(json: jsonObject.object) } else { throw OTTMultiResponseParserError.typeNotFound } - - if let obj = object{ + + if let obj = object { resultArray.append(obj) } } - + return resultArray } else { return [OTTBaseObject]() diff --git a/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift b/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift index 0c81250e..eb5250ab 100644 --- a/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift +++ b/Classes/Providers/OTT/Parsers/OTTObjectMapper.swift @@ -9,18 +9,16 @@ import UIKit import SwiftyJSON - - class OTTObjectMapper: NSObject { static let classNameKey = "objectType" - static let errorKey = "objectType" - + static let errorKey = "error" + static func classByJsonObject(json: Any?) -> OTTBaseObject.Type? { guard let js = json else { return nil } let jsonObject = JSON(js) let className = jsonObject[classNameKey].string - + if let name = className { switch name { case "KalturaLoginResponse": @@ -31,6 +29,10 @@ class OTTObjectMapper: NSObject { return OTTAsset.self case "KalturaLoginSession": return OTTLoginSession.self + case "KalturaPlaybackSource": + return OTTPlaybackSource.self + case "KalturaPlaybackContext": + return OTTPlaybackContext.self default: return nil } diff --git a/Classes/Providers/OTT/Parsers/OTTResponseParser.swift b/Classes/Providers/OTT/Parsers/OTTResponseParser.swift index eb6bcadf..f771ee7d 100644 --- a/Classes/Providers/OTT/Parsers/OTTResponseParser.swift +++ b/Classes/Providers/OTT/Parsers/OTTResponseParser.swift @@ -10,14 +10,14 @@ import UIKit import SwiftyJSON class OTTResponseParser: ResponseParser { - + enum OTTResponseParserError: Error { case typeNotFound case invalidJsonObject } - + static func parse(data:Any) throws -> OTTBaseObject { - + let jsonResponse = JSON(data) let resultObjectJSON = jsonResponse["result"].dictionaryObject let objectType: OTTBaseObject.Type? = OTTObjectMapper.classByJsonObject(json: resultObjectJSON) @@ -27,11 +27,8 @@ class OTTResponseParser: ResponseParser { } else { throw OTTResponseParserError.invalidJsonObject } - }else{ + } else { throw OTTResponseParserError.typeNotFound } } } - - - diff --git a/Classes/Providers/OTT/PhoenixMediaProvider.swift b/Classes/Providers/OTT/PhoenixMediaProvider.swift new file mode 100644 index 00000000..ffda69c2 --- /dev/null +++ b/Classes/Providers/OTT/PhoenixMediaProvider.swift @@ -0,0 +1,393 @@ +// +// OTTEntryProvider.swift +// +// +// Created by Admin on 13/11/2016. +// +// + +import UIKit +import SwiftyJSON + +/************************************************************/ +// MARK: - PhoenixMediaProviderError +/************************************************************/ +public enum PhoenixMediaProviderError: PKError { + + case invalidInputParam(param: String) + case unableToParseData(data: Any) + case noSourcesFound + case serverError(info:String) + + static let domain = "com.kaltura.playkit.error.PhoenixMediaProvider" + + var code: Int { + switch self { + case .invalidInputParam: return 0 + case .unableToParseData: return 1 + case .noSourcesFound: return 2 + case .serverError: return 3 + } + } + + var errorDescription: String { + + switch self { + case .invalidInputParam(let param): return "Invalid input param: \(param)" + case .unableToParseData(let data): return "Unable to parse object" + case .noSourcesFound: return "No source found to play content" + case .serverError(let info): return "Server Error: \(info)" + } + } + + var userInfo: [String: Any] { + return [String: Any]() + } + +} + +/************************************************************/ +// MARK: - PhoenixMediaProvider +/************************************************************/ + +/* Description + + Using Session provider will help you create MediaEntry in order to play content with the player + It's requestig the asset data and creating sources with relevant information for ex' contentURL, licenseURL, fiarPlay certificate and etc' + + #Example of code + ```` + let phoenixMediaProvider = PhoenixMediaProvider() + .set(type: AssetType.media) + .set(assetId: asset.assetID) + .set(fileIds: [file.fileID.stringValue]) + .set(networkProtocol: "https") + .set(playbackContextType: isTrailer ? PlaybackContextType.trailer : PlaybackContextType.playback) + .set(sessionProvider: PhoenixSessionManager.shared) + + phoenixMediaProvider.loadMedia(callback: { (media, error) in + + if let mediaEntry = media, error == nil { + self.player?.prepare(MediaConfig.config(mediaEntry: mediaEntry, startTime: params.startOver ? 0 : asset.currentMediaPositionInSeconds)) + }else{ + print("error loading asset: \(error?.localizedDescription)") + self.delegate?.corePlayer(self, didFailWith:LS("player_error_unable_to_load_entry")) + } + ```` +}) +*/ +@objc public class PhoenixMediaProvider: NSObject, MediaEntryProvider { + + @objc public var sessionProvider: SessionProvider? + @objc public var assetId: String? + @objc public var type: AssetType = .unknown + @objc public var formats: [String]? + @objc public var fileIds: [String]? + @objc public var playbackContextType: PlaybackContextType = .unknown + @objc public var networkProtocol: String? + + public var executor: RequestExecutor? + + public override init() { } + + /// - Parameter sessionProvider: This provider provider the ks for all wroking request. + /// If ks is nil, the provider will load the meida with anonymous ks + /// - Returns: Self ( so you con continue set other parameters after it ) + @discardableResult + @nonobjc public func set(sessionProvider: SessionProvider?) -> Self { + self.sessionProvider = sessionProvider + return self + } + + /// Required parameter + /// + /// - Parameter assetId: asset identifier + /// - Returns: Self + @discardableResult + @nonobjc public func set(assetId: String?) -> Self { + self.assetId = assetId + return self + } + + /// - Parameter type: Asset Object type if it is Media Or EPG + /// - Returns: Self + @discardableResult + @nonobjc public func set(type: AssetType) -> Self { + self.type = type + return self + } + + /// - Parameter playbackContextType: Trailer/Playback/StartOver/Catchup + /// - Returns: Self + @discardableResult + @nonobjc public func set(playbackContextType: PlaybackContextType) -> Self { + self.playbackContextType = playbackContextType + return self + } + + /// - Parameter formats: Asset's requested file formats, + /// According to this formats array order the sources will be ordered in the mediaEntry + /// According to this formats sources will be filtered when creating the mediaEntry + /// - Returns: Self + @discardableResult + @nonobjc public func set(formats: [String]?) -> Self { + self.formats = formats + return self + } + + /// - Parameter formats: Asset's requested file ids, + /// According to this files array order the sources will be ordered in the mediaEntry + /// According to this ids sources will be filtered when creating the mediaEntry + /// - Returns: Self + @discardableResult + @nonobjc public func set(fileIds: [String]?) -> Self { + self.fileIds = fileIds + return self + } + + /// - Parameter networkProtocol: http/https + /// - Returns: Self + @discardableResult + @nonobjc public func set(networkProtocol: String?) -> Self { + self.networkProtocol = networkProtocol + return self + } + + /// - Parameter executor: executor which will be used to send request. + /// default is USRExecutor + /// - Returns: Self + @discardableResult + @nonobjc public func set(executor: RequestExecutor?) -> Self { + self.executor = executor + return self + } + + let defaultProtocol = "https" + + /// This object is created before loading the media in order to make sure all required attributes are set and we are ready to load + struct LoaderInfo { + var sessionProvider: SessionProvider + var assetId: String + var assetType: AssetType + var formats: [String]? + var fileIds: [String]? + var playbackContextType: PlaybackContextType + var networkProtocol: String + var executor: RequestExecutor + + } + + @objc public func loadMedia(callback: @escaping (MediaEntry?, Error?) -> Void) { + guard let sessionProvider = self.sessionProvider else { + callback(nil, PhoenixMediaProviderError.invalidInputParam(param: "sessionProvider" ).asNSError ) + return + } + guard let assetId = self.assetId else { + callback(nil, PhoenixMediaProviderError.invalidInputParam(param: "assetId" ).asNSError) + return + } + guard self.type != .unknown else { + callback(nil, PhoenixMediaProviderError.invalidInputParam(param: "type" ).asNSError) + return + } + guard self.playbackContextType != .unknown else { + callback(nil, PhoenixMediaProviderError.invalidInputParam(param: "contextType" ).asNSError) + return + } + + 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) + + self.startLoad(loaderInfo: loaderParams, callback: callback) + } + + // This is not implemened yet + public func cancel() { + + } + + /// This method is creating the request in order to get playback context, when ks id nil we are adding anonymous login request so some times we will have just get context request and some times we will have multi request with getContext request + anonymouse login + /// - Parameters: + /// - ks: ks if exist + /// - loaderInfo: info regarding entry to load + /// - Returns: request builder + func loaderRequestBuilder(ks: String?, loaderInfo: LoaderInfo) -> KalturaRequestBuilder? { + + let playbackContextOptions = PlaybackContextOptions(playbackContextType: loaderInfo.playbackContextType, protocls: [loaderInfo.networkProtocol], assetFileIds: loaderInfo.fileIds) + + if let token = ks { + + let playbackContextRequest = OTTAssetService.getPlaybackContext(baseURL:loaderInfo.sessionProvider.serverURL, ks: token, assetId: loaderInfo.assetId, type: loaderInfo.assetType, playbackContextOptions: playbackContextOptions ) + return playbackContextRequest + } else { + + let anonymouseLoginRequest = OTTUserService.anonymousLogin(baseURL: loaderInfo.sessionProvider.serverURL, partnerId: loaderInfo.sessionProvider.partnerId) + let ks = "{1:result:ks}" + let playbackContextRequest = OTTAssetService.getPlaybackContext(baseURL:loaderInfo.sessionProvider.serverURL, ks: ks, assetId: loaderInfo.assetId, type: loaderInfo.assetType, playbackContextOptions: playbackContextOptions ) + + guard let req1 = anonymouseLoginRequest, let req2 = playbackContextRequest else { + return nil + } + + let multiRquest = KalturaMultiRequestBuilder(url: loaderInfo.sessionProvider.serverURL)?.setOTTBasicParams() + multiRquest?.add(request: req1).add(request: req2) + return multiRquest + + } + + } + + /// This method is called after all input is valid and we can start loading media + /// + /// - Parameters: + /// - loaderInfo: load info + /// - callback: completion clousor + func startLoad(loaderInfo: LoaderInfo, callback: @escaping (MediaEntry?, Error?) -> Void) { + loaderInfo.sessionProvider.loadKS { (ks, error) in + + guard let requestBuilder: KalturaRequestBuilder = self.loaderRequestBuilder( ks: ks, loaderInfo: loaderInfo) else { + callback(nil, PhoenixMediaProviderError.invalidInputParam(param:"requests params")) + return + } + + let isMultiRequest = requestBuilder is KalturaMultiRequestBuilder + + let request = requestBuilder.set(completion: { (response: Response) in + + var playbackContext: OTTBaseObject? = nil + do { + if (isMultiRequest) { + playbackContext = try OTTMultiResponseParser.parse(data: response.data).last + } else { + playbackContext = try OTTResponseParser.parse(data: response.data) + } + + } catch { + callback(nil, PhoenixMediaProviderError.unableToParseData(data:response.data).asNSError) + } + + if let context = playbackContext as? OTTPlaybackContext { + let media = self.createMediaEntry(loaderInfo: loaderInfo, context: context) + if let sources = media.sources, sources.count > 0 { + callback(media, nil) + } else { + callback(nil, PhoenixMediaProviderError.noSourcesFound.asNSError) + } + } else if let error = playbackContext as? OTTError { + callback(nil, PhoenixMediaProviderError.serverError(info: error.message ?? "Unknown Error").asNSError) + } else { + callback(nil, PhoenixMediaProviderError.unableToParseData(data: response.data).asNSError) + } + }).build() + + loaderInfo.executor.send(request: request) + } + } + + /// Sorting and filtering source accrding to file formats or file ids + func sortedAndFilterSources(by fileIds: [String]?, or fileFormats: [String]?, sources: [OTTPlaybackSource]) -> [OTTPlaybackSource] { + + let orderedSources = sources.filter({ (source: OTTPlaybackSource) -> Bool in + if let formats = fileFormats { + return formats.contains(source.type) + } else if let fileIds = fileIds { + return fileIds.contains("\(source.id)") + } else { + return true + } + }) + .sorted { (source1: OTTPlaybackSource, source2: OTTPlaybackSource) -> Bool in + + if let formats = fileFormats { + let index1 = formats.index(of: source1.type) ?? 0 + let index2 = formats.index(of: source2.type) ?? 0 + return index1 < index2 + } else if let fileIds = fileIds { + + let index1 = fileIds.index(of: "\(source1.id)") ?? 0 + let index2 = fileIds.index(of: "\(source2.id)") ?? 0 + return index1 < index2 + } else { + return false + } + } + + return orderedSources + + } + + func createMediaEntry(loaderInfo: LoaderInfo, context: OTTPlaybackContext) -> MediaEntry { + + let mediaEntry = MediaEntry(id: loaderInfo.assetId) + let sortedSources = self.sortedAndFilterSources(by: loaderInfo.fileIds, or: loaderInfo.formats, sources: context.sources) + + var maxDuration: Float = 0.0 + let mediaSources = sortedSources.flatMap { (source: OTTPlaybackSource) -> MediaSource? in + + let format = FormatsHelper.getMediaFormat(format: source.format, hasDrm: source.drm != nil) + guard FormatsHelper.supportedFormats.contains(format) else { + return nil + } + + var drm: [DRMParams]? = nil + if let drmData = source.drm, drmData.count > 0 { + drm = drmData.flatMap({ (drmData: OTTDrmData) -> DRMParams? in + + let scheme = self.convertScheme(scheme: drmData.scheme) + guard FormatsHelper.supportedSchemes.contains(scheme) else { + return nil + } + + switch scheme { + case .fairplay: + // if the scheme is type fair play and there is no certificate or license URL + guard let certifictae = drmData.certificate + else { return nil } + return FairPlayDRMParams(licenseUri: drmData.licenseURL, scheme: scheme, base64EncodedCertificate: certifictae) + default: + return DRMParams(licenseUri: drmData.licenseURL, scheme: scheme) + } + }) + + // checking if the source is supported with his drm data, cause if the source has drm data but from some reason the mapped drm data is empty the source is not playable + guard let mappedDrmData = drm, mappedDrmData.count > 0 else { + return nil + } + } + + let mediaSource = MediaSource(id: "\(source.id)") + mediaSource.contentUrl = source.url + mediaSource.mediaFormat = format + mediaSource.drmData = drm + + maxDuration = max(maxDuration, source.duration) + return mediaSource + + } + + mediaEntry.sources = mediaSources + mediaEntry.duration = TimeInterval(maxDuration) + + return mediaEntry + + } + + // Mapping between server scheme and local definision of scheme + func convertScheme(scheme: String) -> DRMParams.Scheme { + switch (scheme) { + case "WIDEVINE_CENC": + return .widevineCenc + case "PLAYREADY_CENC": + return .playreadyCenc + case "WIDEVINE": + return .widevineClassic + case "FAIRPLAY": + return .fairplay + default: + return .unknown + } + } + +} diff --git a/Classes/Providers/OTT/Services/OTTAssetService.swift b/Classes/Providers/OTT/Services/OTTAssetService.swift index 66040088..14852321 100644 --- a/Classes/Providers/OTT/Services/OTTAssetService.swift +++ b/Classes/Providers/OTT/Services/OTTAssetService.swift @@ -9,37 +9,53 @@ import UIKit import SwiftyJSON -@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 "" - } - } -} - class OTTAssetService { - static func get(baseURL: String, ks: String, assetId: String, type: AssetType) -> KalturaRequestBuilder? { - + 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: "assetReferenceType", value: JSON(type.asString)) .setBody(key: "type", value: JSON(type.rawValue)) - .setBody(key: "with", value: JSON([["type": "files","objectType": "KalturaCatalogWithHolder"]])) + .setBody(key: "assetReferenceType", value: JSON(type.rawValue)) + .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? { + + if let request: KalturaRequestBuilder = KalturaRequestBuilder(url: baseURL, service: "asset", action: "getPlaybackContext") { + request + .setBody(key: "assetId", value: JSON(assetId)) + .setBody(key: "ks", value: JSON(ks)) + .setBody(key: "assetType", value: JSON(type.rawValue)) + .setBody(key: "contextDataParams", value: JSON(playbackContextOptions.toDictionary())) + return request + } else { + return nil + } + + } } +struct PlaybackContextOptions { + internal var playbackContextType: PlaybackContextType + internal var protocls: [String] + internal var assetFileIds: [String]? + func toDictionary() -> [String: Any] { + + var dict: [String: Any] = [:] + dict["context"] = playbackContextType.rawValue + dict["mediaProtocols"] = protocls + if let fileIds = self.assetFileIds { + dict["assetFileIds"] = fileIds.joined(separator: ",") + } + return dict + } +} diff --git a/Classes/Providers/OTT/Services/OTTLicensedURLService.swift b/Classes/Providers/OTT/Services/OTTLicensedURLService.swift index 5fbee8ef..03a343dd 100644 --- a/Classes/Providers/OTT/Services/OTTLicensedURLService.swift +++ b/Classes/Providers/OTT/Services/OTTLicensedURLService.swift @@ -11,17 +11,16 @@ import SwiftyJSON class OTTLicensedURLService: NSObject { - - internal static func get(baseURL: String, ks: String, fileId: String, fileBaseURL:String) -> KalturaRequestBuilder? { - + 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{ + } else { return nil } } - + } diff --git a/Classes/Providers/OTT/Services/OTTSessionService.swift b/Classes/Providers/OTT/Services/OTTSessionService.swift index 1f29a248..bfd04165 100644 --- a/Classes/Providers/OTT/Services/OTTSessionService.swift +++ b/Classes/Providers/OTT/Services/OTTSessionService.swift @@ -11,17 +11,29 @@ import SwiftyJSON internal class OTTSessionService: NSObject { - - internal static func get(baseURL:String,ks:String) -> KalturaRequestBuilder? { - + 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{ + } 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 new file mode 100644 index 00000000..d490a8aa --- /dev/null +++ b/Classes/Providers/OTT/Services/OTTSocialService.swift @@ -0,0 +1,41 @@ +// +// 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 index 259f0940..596cd037 100644 --- a/Classes/Providers/OTT/Services/OTTUserService.swift +++ b/Classes/Providers/OTT/Services/OTTUserService.swift @@ -11,32 +11,57 @@ import SwiftyJSON public class OTTUserService: NSObject { - static func login(baseURL: String, partnerId: Int64, username: String, password: String) -> KalturaRequestBuilder? { - + 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 } - - static func refreshSession(baseURL: String, refreshToken: String, ks: String) -> KalturaRequestBuilder? { + + 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 } - - static func anonymousLogin(baseURL: String, partnerId: Int64) -> KalturaRequestBuilder? { + + 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/Services/PhoenixAPIDefines.swift b/Classes/Providers/OTT/Services/PhoenixAPIDefines.swift new file mode 100644 index 00000000..9255c999 --- /dev/null +++ b/Classes/Providers/OTT/Services/PhoenixAPIDefines.swift @@ -0,0 +1,44 @@ +// +// PhoenixAPIDefines.swift +// Pods +// +// Created by Rivka Peleg on 02/03/2017. +// +// + +import Foundation + + +@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 "" + } + } +} diff --git a/Classes/Providers/OTT/Session/OTTSessionManager.swift b/Classes/Providers/OTT/Session/OTTSessionManager.swift index 5549aff5..eaa5f26e 100644 --- a/Classes/Providers/OTT/Session/OTTSessionManager.swift +++ b/Classes/Providers/OTT/Session/OTTSessionManager.swift @@ -8,188 +8,360 @@ 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 failedToGetKS + + enum SessionManagerError: Error { + case failed case failedToGetLoginResponse case failedToRefreshKS - case failedToBuildRefreshRequest - case invalidRefreshCallResponse - case noRefreshTokenOrTokenToRefresh - case failedToParseResponse - } - - let saftyMargin = 5*60.0 - + 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 - - private var ks: String? - private var refreshToken: String? - private var tokenExpiration: Date? - + + 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{ + 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) } - - @objc public func startSession(username: String, password: String, completion: @escaping (_ error: Error?) -> Void) -> Void { - + + /************************************************************/ + // 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) - + password: password, + udid: udid) + let sessionGetRequest = OTTSessionService.get(baseURL: self.serverURL, - ks:"1:result:loginSession:ks") - - if let r1 = loginRequestBuilder, let r2 = sessionGetRequest { - - let mrb = KalturaMultiRequestBuilder(url: self.serverURL)?.add(request: r1).add(request: r2).setOTTBasicParams() - mrb?.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.ks = loginObj.loginSession?.ks - self.refreshToken = loginObj.loginSession?.refreshToken - self.tokenExpiration = sessionObj.tokenExpiration - - } - completion(nil) - } else { - completion(SessionManagerError.failedToGetLoginResponse) - } - } else { - completion(SessionManagerError.failedToGetLoginResponse) - } - }) - - if let request = mrb?.build() { - self.executor.send(request: request) + 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 } } - - @objc public func startAnonymousSession(completion:@escaping (_ error: Error?) -> Void) { - + + /************************************************************/ + // 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") - - if let r1 = loginRequestBuilder, let r2 = sessionGetRequest { - - let mrb = KalturaMultiRequestBuilder(url: self.serverURL)?.add(request: r1) - .setOTTBasicParams() - .add(request: r2).setOTTBasicParams() - .set(completion: { (r:Response) in - - if let data = r.data - { + 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) + result = try OTTMultiResponseParser.parse(data:data) } catch { completion(error) } - - if let result = result, result.count == 2, let loginSession = result[0] as? OTTLoginSession, let session = result[1] as? OTTSession{ - - self.ks = loginSession.ks - self.refreshToken = loginSession.refreshToken - self.tokenExpiration = session.tokenExpiration - completion(nil) + + 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.failedToGetLoginResponse) + completion(SessionManagerError.failed) } } else { - completion(SessionManagerError.failedToGetLoginResponse) + completion(SessionManagerError.failed) } }) - - if let request = mrb?.build() { - self.executor.send(request: request) - } - } - } - - @objc public func loadKS(completion: @escaping (String?, Error?) -> Void) { - let now = Date() - - if let expiration = self.tokenExpiration, expiration.timeIntervalSince(now) > saftyMargin { - completion(self.ks, nil) - } else { - self.refreshKS(completion: completion) - } - } - - @objc public func refreshKS(completion: @escaping (String?, Error?) -> Void) { - - guard let refreshToken = self.refreshToken, let ks = self.ks else { - completion(nil, SessionManagerError.noRefreshTokenOrTokenToRefresh) - return - } - - let refreshSessionRequest = OTTUserService.refreshSession(baseURL: self.serverURL, refreshToken: refreshToken, ks: ks) - let getSessionRequest = OTTSessionService.get(baseURL: self.serverURL, ks: "1:result:ks") - - guard let req1 = refreshSessionRequest, let req2 = getSessionRequest else { - completion(nil, SessionManagerError.invalidRefreshCallResponse) - return - } - - let mrb: KalturaMultiRequestBuilder? = (KalturaMultiRequestBuilder(url: self.serverURL)?.add(request: req1).add(request: req2))?.setOTTBasicParams().set(completion: { (r:Response) in - - guard let data = r.data else { - completion(nil, SessionManagerError.failedToRefreshKS) - return - } - - var response: [OTTBaseObject]? = nil - do { - response = try OTTMultiResponseParser.parse(data: data) - } catch { - completion(nil, error) - } - - if let response = response, response.count == 2, let loginSession = response[0] as? OTTLoginSession, let session = response[1] as? OTTSession { - - self.ks = loginSession.ks - self.refreshToken = loginSession.refreshToken - self.tokenExpiration = session.tokenExpiration - completion(self.ks, nil) - } else { - completion(nil, SessionManagerError.failedToRefreshKS) - } - }) - - if let request = mrb?.build() { + + let request = request.build() self.executor.send(request: request) - } else { - completion(nil, SessionManagerError.failedToBuildRefreshRequest) - } } -} +} diff --git a/Classes/Providers/OVP/Model/OVPSource.swift b/Classes/Providers/OVP/Model/OVPSource.swift index 2aaf0f3e..3218267c 100644 --- a/Classes/Providers/OVP/Model/OVPSource.swift +++ b/Classes/Providers/OVP/Model/OVPSource.swift @@ -12,7 +12,7 @@ import SwiftyJSON class OVPSource: OVPBaseObject { var deliveryProfileId: Int64 - var format: String? + var format: String var protocols: [String]? var flavors: [String]? var url: URL? @@ -31,12 +31,14 @@ class OVPSource: OVPBaseObject { let jsonObject = JSON(json) - guard let id = jsonObject[deliveryProfileIdKey].int64 else { - return nil + guard let id = jsonObject[deliveryProfileIdKey].int64, + let format = jsonObject[formatKey].string + else { + return nil } self.deliveryProfileId = id - self.format = jsonObject[formatKey].string + self.format = format if let protocols = jsonObject[protocolsKey].string{ self.protocols = protocols.components(separatedBy: ",") } diff --git a/Classes/Providers/OVP/OVPMediaProvider.swift b/Classes/Providers/OVP/OVPMediaProvider.swift index c8915f30..5cefa1b2 100644 --- a/Classes/Providers/OVP/OVPMediaProvider.swift +++ b/Classes/Providers/OVP/OVPMediaProvider.swift @@ -184,7 +184,8 @@ import SwiftyXMLParser var mediaSources: [MediaSource] = [MediaSource]() sources.forEach { (source: OVPSource) in //detecting the source type - let format = self.getSourceFormat(source: source) + + let format = FormatsHelper.getMediaFormat(format: source.format, hasDrm: source.drm != nil) //If source type is not supported source will not be created guard format != .unknown else { return } @@ -255,26 +256,8 @@ import SwiftyXMLParser } - // This method decding the source type base on scheck and drm data - private func getSourceFormat(source: OVPSource) -> MediaSource.MediaFormat { - - if let format = source.format { - switch format { - case "applehttp": - return .hls - case "url": - if source.drm == nil { - return .mp4 - } else { - return .wvm - } - default: - return .unknown - } - } - - return .unknown - } + + // Creating the drm data based on scheme private func buildDRMParams(drm: [OVPDRM]?) -> [DRMParams]? { @@ -306,7 +289,7 @@ import SwiftyXMLParser // building the url with the SourceBuilder class private func playbackURL(loadInfo: LoaderInfo, source: OVPSource, ks: String?) -> URL? { - let formatType = self.getSourceFormat(source: source) + let formatType = FormatsHelper.getMediaFormat(format: source.format, hasDrm: source.drm != nil) var playURL: URL? = nil if let flavors = source.flavors, flavors.count > 0 { diff --git a/Example/PlayKit.xcodeproj/project.pbxproj b/Example/PlayKit.xcodeproj/project.pbxproj index f40b0675..b51da04c 100644 --- a/Example/PlayKit.xcodeproj/project.pbxproj +++ b/Example/PlayKit.xcodeproj/project.pbxproj @@ -22,14 +22,14 @@ 8460889C1DD1AB1A009E0E7A /* Entries.json in Resources */ = {isa = PBXBuildFile; fileRef = 8460889B1DD1AB1A009E0E7A /* Entries.json */; }; BF5C80821DF0E36500D3E665 /* MessegeBusTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5C80811DF0E36500D3E665 /* MessegeBusTest.swift */; }; C23A62D11DF413FF00635FA2 /* MockMediaProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62C91DF412FD00635FA2 /* MockMediaProviderTest.swift */; }; - C23A62D21DF4140B00635FA2 /* OTTMediaProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62CA1DF412FD00635FA2 /* OTTMediaProviderTest.swift */; }; - C23A62D31DF4140E00635FA2 /* OVPMediaProviederTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23A62CB1DF412FD00635FA2 /* OVPMediaProviederTest.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 */; }; + C2D803221E6C09CF00A3DE15 /* SourceSelectorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB09C99B1E28072900D3671F /* SourceSelectorTest.swift */; }; C63FA2B01DF3F854004030E0 /* PlayerControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63FA2AE1DF3F854004030E0 /* PlayerControllerTest.swift */; }; - FB09C99C1E28072900D3671F /* SourceSelectorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB09C99B1E28072900D3671F /* SourceSelectorTest.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -69,7 +69,7 @@ BE76A2A9EC2DDAC016A40200 /* Pods_PlayKit_PlayKit_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PlayKit_PlayKit_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BF5C80811DF0E36500D3E665 /* MessegeBusTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessegeBusTest.swift; sourceTree = ""; }; C23A62C91DF412FD00635FA2 /* MockMediaProviderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMediaProviderTest.swift; sourceTree = ""; }; - C23A62CA1DF412FD00635FA2 /* OTTMediaProviderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTTMediaProviderTest.swift; sourceTree = ""; }; + 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 = ""; }; @@ -242,7 +242,7 @@ children = ( C23A62DB1DF47D9800635FA2 /* OTTSessionProviderTest.swift */, C23A62C91DF412FD00635FA2 /* MockMediaProviderTest.swift */, - C23A62CA1DF412FD00635FA2 /* OTTMediaProviderTest.swift */, + C23A62CA1DF412FD00635FA2 /* PhoenixMediaProviderTest.swift */, C23A62CB1DF412FD00635FA2 /* OVPMediaProviederTest.swift */, C23A62CF1DF4131700635FA2 /* MediaEntryProviderMock.swift */, ); @@ -476,14 +476,14 @@ files = ( C23A62D41DF4141000635FA2 /* MediaEntryProviderMock.swift in Sources */, C23A62D11DF413FF00635FA2 /* MockMediaProviderTest.swift in Sources */, - C23A62D31DF4140E00635FA2 /* OVPMediaProviederTest.swift in Sources */, + C2D803211E6C091600A3DE15 /* OVPMediaProviederTest.swift in Sources */, 20CD64591E4C9697002C9401 /* PluginTestConfiguration.swift in Sources */, C63FA2B01DF3F854004030E0 /* PlayerControllerTest.swift in Sources */, + C2D803221E6C09CF00A3DE15 /* SourceSelectorTest.swift in Sources */, 2012AFB21E4B860B00BBA61C /* PhoenixPluginTest.swift in Sources */, BF5C80821DF0E36500D3E665 /* MessegeBusTest.swift in Sources */, 2012AFB41E4B872300BBA61C /* PlayerCreator.swift in Sources */, - FB09C99C1E28072900D3671F /* SourceSelectorTest.swift in Sources */, - C23A62D21DF4140B00635FA2 /* OTTMediaProviderTest.swift in Sources */, + C23A62D21DF4140B00635FA2 /* PhoenixMediaProviderTest.swift in Sources */, 2012AFAF1E4B85CA00BBA61C /* OTTAnalyticsPluginTest.swift in Sources */, C23A62DD1DF47D9C00635FA2 /* OTTSessionProviderTest.swift in Sources */, ); diff --git a/Example/Podfile.lock b/Example/Podfile.lock index a02d6c7c..bd46cf89 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -67,6 +67,6 @@ SPEC CHECKSUMS: Youbora-AVPlayer: 02aea2a12a4f7e6a61d8a1747e5dfc177bf2354b Youbora-YouboraLib: 523adf7cd09c4a213e3485e6ec7c48d498986246 -PODFILE CHECKSUM: 25eed3c6d61ea6aa08f2373344715de01155899a +PODFILE CHECKSUM: 8702b84cf4a02b3d2de2ccf0fe915659f256d0e6 COCOAPODS: 1.2.0.beta.1 diff --git a/Example/Tests/Analytics/OTTAnalyticsPluginTest.swift b/Example/Tests/Analytics/OTTAnalyticsPluginTest.swift index ade63246..b6334fa5 100644 --- a/Example/Tests/Analytics/OTTAnalyticsPluginTest.swift +++ b/Example/Tests/Analytics/OTTAnalyticsPluginTest.swift @@ -123,8 +123,8 @@ class OTTAnalyticsPluginTest: QuickSpec { var analyticsPluginMock = analyticsPluginMock print("received analytics event: \(eventType.rawValue)") switch eventType { - case .first_play, .play: - if eventType == .first_play { + case .firstPlay, .play: + if eventType == .firstPlay { analyticsPluginMock.invocationCount.firstPlayCount += 1 DispatchQueue.main.asyncAfter(deadline: .now() + 1) { player.pause() @@ -149,9 +149,9 @@ class OTTAnalyticsPluginTest: QuickSpec { analyticsPluginMock.finishedHandling = true } } - if eventType == .first_play || eventType == .play { + if eventType == .firstPlay || eventType == .play { switch analyticsPluginMock.invocationCount.playCount { - case 0: expect(eventType).to(equal(OTTAnalyticsEventType.first_play)) + case 0: expect(eventType).to(equal(OTTAnalyticsEventType.firstPlay)) default: expect(eventType).to(equal(OTTAnalyticsEventType.play)) } analyticsPluginMock.invocationCount.playCount += 1 diff --git a/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift b/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift index d2ce1d5d..74de74a3 100644 --- a/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift +++ b/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift @@ -42,9 +42,8 @@ class MockMediaProviderTest: XCTestCase { .set(url: self.filePath!) .set(id: "m001") - mediaProvider1.loadMedia { (r:Result) in - print(r) - if r.data != nil { + mediaProvider1.loadMedia { (media, error) in + if media != nil { theExeption.fulfill() } else{ @@ -64,8 +63,8 @@ class MockMediaProviderTest: XCTestCase { let theExeption = expectation(description: "test") let mediaProvider2 : MediaEntryProvider = MockMediaEntryProvider().set(url: self.filePath!).set(id: "sdf") - mediaProvider2.loadMedia { (r:Result) in - if r.error != nil { + mediaProvider2.loadMedia { (media, error) in + if error != nil { theExeption.fulfill() }else{ XCTFail() @@ -84,8 +83,8 @@ class MockMediaProviderTest: XCTestCase { let theExeption = expectation(description: "test") let mediaProvider2 : MediaEntryProvider = MockMediaEntryProvider().set(url: URL(string:"asdd")).set(id: "sdf") - mediaProvider2.loadMedia { (r:Result) in - if r.error != nil { + mediaProvider2.loadMedia { (media, error) in + if error != nil { theExeption.fulfill() }else{ XCTFail() @@ -106,8 +105,8 @@ class MockMediaProviderTest: XCTestCase { .set(content: self.fileContent) .set(id: "m001") - mediaProvider1.loadMedia { (r:Result) in - if let mediaEntry = r.data { + mediaProvider1.loadMedia { (media, error) in + if let mediaEntry = media { if let sources = mediaEntry.sources, sources.count > 0{ let source = sources[0] diff --git a/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift b/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift deleted file mode 100644 index cd0a3c77..00000000 --- a/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// OTTMediaProviderTest.swift -// PlayKit -// -// Created by Rivka Peleg on 04/12/2016. -// Copyright © 2016 CocoaPods. All rights reserved. -// - -import XCTest -import PlayKit - - - -class OTTMediaProviderTest: XCTestCase, SessionProvider { - - - - - let mediaID = "258656" - var partnerId: Int64 = 198 - var serverURL: String = "http://52.210.223.65:8080/v4_0/api_v3" - - public func loadKS(completion: @escaping (Result) -> Void) { - completion(Result(data: "djJ8MTk4fLsl2jWZfTLBHh80n32POkgauZLWcLXhEEySDRL9yRtOLtr92sPWaKpnCaz4nJgsjjXIxD6PkOLXlOvpEHV3Wizc384sF3F4Kj1MfiqJRQd8", error: nil)) - } - - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - - func testRegularCaseTest() { - - let theExeption = expectation(description: "test") - - let provider = OTTMediaProvider() - .set(sessionProvider: self) - .set(mediaId: mediaID) - .set(type: AssetType.media) - .set(formats: ["Mobile_Devices_Main_HD"]) - - provider.loadMedia { (r:Result) in - print(r) - if (r.error != nil){ - theExeption.fulfill() - }else{ - XCTFail() - } - } - - self.waitForExpectations(timeout: 6.0) { (_) -> Void in - - } - } -} - - diff --git a/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift b/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift index e305e734..82c44ae7 100644 --- a/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift +++ b/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift @@ -21,12 +21,11 @@ class OTTSessionProviderTest: XCTestCase { func testOTTSessionProvider() { - let sessionProvider = OTTSessionManager(serverURL:"http://52.210.223.65:8080/v4_0/api_v3", partnerId:198, executor: nil) + 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: { (r:Result) in - print(r.data) - + sessionProvider.loadKS(completion: { (ks, error) in + print(ks ?? "") }) }else{ diff --git a/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift b/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift index 2f0f0be2..85724b26 100644 --- a/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift +++ b/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift @@ -14,8 +14,8 @@ import PlayKit class OVPMediaProviederTest: XCTestCase, SessionProvider { - public func loadKS(completion: @escaping (Result) -> Void) { - completion(Result(data: "djJ8MjIyMjQwMXwdcXO1uXvBNZYxpUCxIGfEN120AWUJGJYCTt2qbhE3hCXa62-TGAOrxUtA0WwBGCqRreBaAzd2Dnejy9bYmcqtC1SxtCkZjw_jwoFd4Y3Cl-9hYgSCTcLRdqiePConBm8=", error: nil)) + public func loadKS(completion: @escaping (String?, Error?) -> Void) { + completion("", nil) } let entryID = "1_ytsd86sc" @@ -40,13 +40,13 @@ class OVPMediaProviederTest: XCTestCase, SessionProvider { .set(entryId: self.entryID) .set(executor: MediaEntryProviderMockExecutor(entryID: entryID, domain: "ovp")) - provider.loadMedia { (r:Result) in - if (r.error != nil){ + provider.loadMedia { (media, error) in + if (error != nil){ XCTFail() }else{ theExeption.fulfill() } - print(r) + } @@ -65,13 +65,12 @@ class OVPMediaProviederTest: XCTestCase, SessionProvider { .set(executor: USRExecutor.shared ) - provider.loadMedia { (r:Result) in - if (r.error != nil){ + provider.loadMedia { (media, error) in + if (error != nil){ XCTFail() }else{ theExeption.fulfill() } - print(r) } diff --git a/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift b/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift new file mode 100644 index 00000000..6c3252b4 --- /dev/null +++ b/Example/Tests/MediaEntryProvider/PhoenixMediaProviderTest.swift @@ -0,0 +1,58 @@ +// +// PhoenixMediaProviderTest.swift +// PlayKit +// +// Created by Rivka Peleg on 04/12/2016. +// Copyright © 2016 CocoaPods. All rights reserved. +// + +import XCTest +import PlayKit + + + +class PhoenixMediaProviderTest: XCTestCase, SessionProvider { + + + + + let mediaID = "485293" + var partnerId: Int64 = 198 + var serverURL: String = "http://api-preprod.ott.kaltura.com/v4_2/api_v3" + + + public func loadKS(completion: @escaping (String?, Error?) -> Void) { + completion(nil, nil) + } + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testRegularCaseTest() { + + let theExeption = expectation(description: "test") + + let provider = PhoenixMediaProvider() + .set(sessionProvider: self) + .set(assetId: mediaID) + .set(type: AssetType.media) + .set(playbackContextType: PlaybackContextType.playback) + + + provider.loadMedia { (entry, error) in + print(entry ?? "") + theExeption.fulfill() + } + + self.waitForExpectations(timeout: 6.0) { (_) -> Void in + + } + } +} + + diff --git a/Example/Tests/MessegeBusTest.swift b/Example/Tests/MessegeBusTest.swift index ef9bf06e..8e4ffe4e 100644 --- a/Example/Tests/MessegeBusTest.swift +++ b/Example/Tests/MessegeBusTest.swift @@ -29,7 +29,9 @@ class MessegeBusTest: XCTestCase { entry["sources"] = sources let mediaConfig = MediaConfig(mediaEntry: MediaEntry(json: entry)) - self.player = PlayKitManager.shared.loadPlayer(pluginConfig: nil) + do{ + self.player = try PlayKitManager.shared.loadPlayer(pluginConfig: nil) + }catch{} self.player.prepare(mediaConfig) } diff --git a/Example/Tests/PlayerControllerTest.swift b/Example/Tests/PlayerControllerTest.swift index 065409c2..8b55b960 100644 --- a/Example/Tests/PlayerControllerTest.swift +++ b/Example/Tests/PlayerControllerTest.swift @@ -32,7 +32,11 @@ class PlayerControllerTest: XCTestCase { entry["sources"] = sources let mediaConfig = MediaConfig(mediaEntry: MediaEntry(json: entry)) - self.player = PlayKitManager.shared.loadPlayer(pluginConfig: nil) + do{ + self.player = try PlayKitManager.shared.loadPlayer(pluginConfig: nil) + } catch { + + } self.player.prepare(mediaConfig) } diff --git a/Example/Tests/PlayerCreator.swift b/Example/Tests/PlayerCreator.swift index 8183f720..b0ffa10c 100644 --- a/Example/Tests/PlayerCreator.swift +++ b/Example/Tests/PlayerCreator.swift @@ -41,20 +41,28 @@ extension PlayerCreator where Self: QuickSpec { entry["id"] = "test" entry["sources"] = sources let mediaConfig = MediaConfig(mediaEntry: MediaEntry(json: entry)) - + do{ let pluginConfig: PluginConfig? if let pluginConfigDict = pluginConfigDict { let pluginConfig = PluginConfig(config: pluginConfigDict) - player = PlayKitManager.shared.loadPlayer(pluginConfig: pluginConfig) as! PlayerLoader + + + player = try PlayKitManager.shared.loadPlayer(pluginConfig: pluginConfig) as! PlayerLoader } else { pluginConfig = nil - player = PlayKitManager.shared.loadPlayer(pluginConfig: pluginConfig) as! PlayerLoader + player = try PlayKitManager.shared.loadPlayer(pluginConfig: pluginConfig) as! PlayerLoader } - - if shouldStartPreparing { - player.prepare(mediaConfig) + + if shouldStartPreparing { + player.prepare(mediaConfig) + } + return player + }catch{ + } - return player + + return PlayerLoader() + } } From 82ed5f68197b057e2a1d6cc20a3903988e60120d Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Tue, 28 Mar 2017 16:56:20 +0300 Subject: [PATCH 07/27] #FEM-1296 (#125) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Prevented ads player decorator from sending play() after post-roll content resume. * Fixed typo with AdInformation event was missing ‘r’. --- .../Player/AVPlayerEngine/AVPlayerEngine.swift | 2 +- Plugins/IMA/AdsEnabledPlayerController.swift | 17 +++++++++++++++-- Plugins/IMA/IMAPlugin.swift | 2 +- Plugins/IMA/PKAdInfo.swift | 4 ++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift index 269363c7..6d62be23 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift @@ -197,7 +197,7 @@ class AVPlayerEngine: AVPlayer { func destroy() { PKLog.trace("destory player") self.nonObservablePropertiesUpdateTimer?.invalidate() - self.nonObservablePropertiesUpdateTimer == nil + self.nonObservablePropertiesUpdateTimer = nil self.removeObservers() self.avPlayerLayer = nil self._view = nil diff --git a/Plugins/IMA/AdsEnabledPlayerController.swift b/Plugins/IMA/AdsEnabledPlayerController.swift index 7684e8c4..c66289d5 100644 --- a/Plugins/IMA/AdsEnabledPlayerController.swift +++ b/Plugins/IMA/AdsEnabledPlayerController.swift @@ -15,6 +15,11 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl var isAdPlayback = false var isPlayEnabled = false + + /// 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. + var shouldPreventContentResume = false + var adsPlugin: AdsPlugin! weak var messageBus: MessageBus? @@ -89,13 +94,21 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl func adsPlugin(_ adsPlugin: AdsPlugin, didReceive event: PKEvent) { if event is AdEvent.AdDidRequestPause { - super.pause() self.isAdPlayback = true + super.pause() } else if event is AdEvent.AdDidRequestResume { - super.play() self.isAdPlayback = false + if !self.shouldPreventContentResume { + super.resume() + } } else if event is AdEvent.AdResumed { self.isPlayEnabled = true + } else if event is AdEvent.AdInformation { + if event.adInfo?.positionType == .postRoll { + self.shouldPreventContentResume = true + } + } else if event is AdEvent.AllAdsCompleted { + self.shouldPreventContentResume = false } } } diff --git a/Plugins/IMA/IMAPlugin.swift b/Plugins/IMA/IMAPlugin.swift index 5b5d9ca3..e4a1c6ce 100644 --- a/Plugins/IMA/IMAPlugin.swift +++ b/Plugins/IMA/IMAPlugin.swift @@ -301,7 +301,7 @@ extension IMAAdsManager { case .STARTED: if let ad = event.ad { let adInfo = PKAdInfo(ad: ad) - self.notify(event: AdEvent.AdInfomation(adInfo: adInfo)) + self.notify(event: AdEvent.AdInformation(adInfo: adInfo)) } self.notify(event: AdEvent.AdStarted()) self.showLoadingView(false, alpha: 0) diff --git a/Plugins/IMA/PKAdInfo.swift b/Plugins/IMA/PKAdInfo.swift index 04392db7..b928f053 100644 --- a/Plugins/IMA/PKAdInfo.swift +++ b/Plugins/IMA/PKAdInfo.swift @@ -91,9 +91,9 @@ import GoogleInteractiveMediaAds extension AdEvent { - @objc public static let adInformation: AdEvent.Type = AdInfomation.self + @objc public static let adInformation: AdEvent.Type = AdInformation.self - class AdInfomation: AdEvent { + class AdInformation: AdEvent { convenience init(adInfo: PKAdInfo) { self.init([AdEventDataKeys.adInfo: adInfo]) } From 2a63332e124489c7a11659e01e9de89cb415c48e Mon Sep 17 00:00:00 2001 From: Eliza Sapir Date: Tue, 28 Mar 2017 17:08:21 +0300 Subject: [PATCH 08/27] bump 0.1.27 --- Example/PlayKit/Info.plist | 2 +- PlayKit.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/PlayKit/Info.plist b/Example/PlayKit/Info.plist index fa1d1ef2..e0312f0d 100644 --- a/Example/PlayKit/Info.plist +++ b/Example/PlayKit/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.1.26 + 0.1.27 CFBundleSignature ???? CFBundleVersion diff --git a/PlayKit.podspec b/PlayKit.podspec index 984e1cb2..ce20aa4e 100644 --- a/PlayKit.podspec +++ b/PlayKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'PlayKit' -s.version = '0.1.26' +s.version = '0.1.27' s.summary = 'PlayKit: Kaltura Mobile Player SDK - iOS' From be9ccbad55d6d511c4777a258f7a14f22e52375b Mon Sep 17 00:00:00 2001 From: Eliza Sapir Date: Tue, 28 Mar 2017 17:48:30 +0300 Subject: [PATCH 09/27] bump 0.1.28 --- Example/PlayKit/Info.plist | 2 +- PlayKit.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/PlayKit/Info.plist b/Example/PlayKit/Info.plist index e0312f0d..86e78c0a 100644 --- a/Example/PlayKit/Info.plist +++ b/Example/PlayKit/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.1.27 + 0.1.28 CFBundleSignature ???? CFBundleVersion diff --git a/PlayKit.podspec b/PlayKit.podspec index ce20aa4e..c9abfd90 100644 --- a/PlayKit.podspec +++ b/PlayKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'PlayKit' -s.version = '0.1.27' +s.version = '0.1.28' s.summary = 'PlayKit: Kaltura Mobile Player SDK - iOS' From 4e5e3114e46d98ef6c8a1f178a5433403fc56f26 Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Wed, 29 Mar 2017 15:39:06 +0300 Subject: [PATCH 10/27] #FEM-1299 (#126) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed issue where adsManager initializing would crash when `IMAContentPlayhead` protocol’s `currentTime` property would return a NaN value. Now makes a check `isNaN` to make sure it works. --- Plugins/IMA/IMAPlugin.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Plugins/IMA/IMAPlugin.swift b/Plugins/IMA/IMAPlugin.swift index e4a1c6ce..8db03ebe 100644 --- a/Plugins/IMA/IMAPlugin.swift +++ b/Plugins/IMA/IMAPlugin.swift @@ -38,8 +38,17 @@ extension IMAAdsManager { private var startAdCalled = false private var loaderFailed = false + /************************************************************/ + // MARK: - IMAContentPlayhead + /************************************************************/ + public var currentTime: TimeInterval { - return self.player?.currentTime ?? 0 + // IMA must receive a number value so we must check `isNaN` on any value we send. + // Before returning `player.currentTime` we need to check `!player.currentTime.isNaN`. + if let currentTime = self.player?.currentTime, !currentTime.isNaN { + return currentTime + } + return 0 } /************************************************************/ From 56df857a642c933ddf22472ef651599504c7e0b3 Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Wed, 29 Mar 2017 22:30:41 +0300 Subject: [PATCH 11/27] Move AdInfo to AdStarted event (#127) * Aligned ad information event with android. Now information is inside ad started event. * *Fixed issues with `PKAdInfo` --- Classes/PlayerEvent.swift | 7 ++++- .../Plugins/Ads}/PKAdInfo.swift | 30 ++----------------- Plugins/IMA/AdsEnabledPlayerController.swift | 14 ++++----- Plugins/IMA/IMAPlugin.swift | 26 ++++++++++++---- 4 files changed, 36 insertions(+), 41 deletions(-) rename {Plugins/IMA => Classes/Plugins/Ads}/PKAdInfo.swift (72%) diff --git a/Classes/PlayerEvent.swift b/Classes/PlayerEvent.swift index 22f71c45..835d5bac 100644 --- a/Classes/PlayerEvent.swift +++ b/Classes/PlayerEvent.swift @@ -154,7 +154,12 @@ import AVFoundation /// Sent when an error occurs. @objc public static let error: AdEvent.Type = Error.self - class AdStarted: AdEvent {} + class AdStarted: AdEvent { + convenience init(adInfo: PKAdInfo) { + self.init([AdEventDataKeys.adInfo: adInfo]) + } + } + class AdBreakReady: AdEvent {} class AdBreakEnded: AdEvent {} class AdBreakStarted: AdEvent {} diff --git a/Plugins/IMA/PKAdInfo.swift b/Classes/Plugins/Ads/PKAdInfo.swift similarity index 72% rename from Plugins/IMA/PKAdInfo.swift rename to Classes/Plugins/Ads/PKAdInfo.swift index b928f053..3dad2650 100644 --- a/Plugins/IMA/PKAdInfo.swift +++ b/Classes/Plugins/Ads/PKAdInfo.swift @@ -7,14 +7,15 @@ // import Foundation -import GoogleInteractiveMediaAds +/// The position type of the ad according to the content timeline. @objc public enum AdPositionType: Int { case preRoll case midRoll case postRoll } +/// `PKAdInfo` represents ad information. @objc public class PKAdInfo: NSObject { @objc public var adDescription: String @@ -46,21 +47,6 @@ import GoogleInteractiveMediaAds } } - init(ad: IMAAd) { - self.adDescription = ad.adDescription - self.duration = ad.duration - self.title = ad.adTitle - self.isSkippable = ad.isSkippable - self.contentType = ad.contentType - self.adId = ad.adId - self.adSystem = ad.adSystem - self.height = Int(ad.height) - self.width = Int(ad.width) - self.podCount = Int(ad.adPodInfo.totalAds) - self.podPosition = Int(ad.adPodInfo.adPosition) - self.podTimeOffset = ad.adPodInfo.timeOffset - } - init(adDescription: String, adDuration: TimeInterval, title: String, @@ -89,19 +75,7 @@ import GoogleInteractiveMediaAds } } -extension AdEvent { - - @objc public static let adInformation: AdEvent.Type = AdInformation.self - - class AdInformation: AdEvent { - convenience init(adInfo: PKAdInfo) { - self.init([AdEventDataKeys.adInfo: adInfo]) - } - } -} - extension PKEvent { - /// Ad info, PKEvent Ad Data Accessor @objc public var adInfo: PKAdInfo? { return self.data?[AdEventDataKeys.adInfo] as? PKAdInfo diff --git a/Plugins/IMA/AdsEnabledPlayerController.swift b/Plugins/IMA/AdsEnabledPlayerController.swift index c66289d5..77c2e286 100644 --- a/Plugins/IMA/AdsEnabledPlayerController.swift +++ b/Plugins/IMA/AdsEnabledPlayerController.swift @@ -93,22 +93,22 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl } func adsPlugin(_ adsPlugin: AdsPlugin, didReceive event: PKEvent) { - if event is AdEvent.AdDidRequestPause { + switch event { + case let e where type(of: e) == AdEvent.adDidRequestPause: self.isAdPlayback = true super.pause() - } else if event is AdEvent.AdDidRequestResume { + case let e where type(of: e) == AdEvent.adDidRequestResume: self.isAdPlayback = false if !self.shouldPreventContentResume { super.resume() } - } else if event is AdEvent.AdResumed { - self.isPlayEnabled = true - } else if event is AdEvent.AdInformation { + case let e where type(of: e) == AdEvent.adResumed: self.isPlayEnabled = true + case let e where type(of: e) == AdEvent.adStarted: if event.adInfo?.positionType == .postRoll { self.shouldPreventContentResume = true } - } else if event is AdEvent.AllAdsCompleted { - self.shouldPreventContentResume = false + case let e where type(of: e) == AdEvent.allAdsCompleted: self.shouldPreventContentResume = false + default: break } } } diff --git a/Plugins/IMA/IMAPlugin.swift b/Plugins/IMA/IMAPlugin.swift index 8db03ebe..29e782c0 100644 --- a/Plugins/IMA/IMAPlugin.swift +++ b/Plugins/IMA/IMAPlugin.swift @@ -14,6 +14,25 @@ extension IMAAdsManager { } } +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 + ) + } +} + @objc public class IMAPlugin: BasePlugin, PKPluginWarmUp, PlayerDecoratorProvider, AdsPlugin, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMAWebOpenerDelegate, IMAContentPlayhead { weak var dataSource: AdsPluginDataSource? { @@ -308,11 +327,8 @@ extension IMAAdsManager { } } case .STARTED: - if let ad = event.ad { - let adInfo = PKAdInfo(ad: ad) - self.notify(event: AdEvent.AdInformation(adInfo: adInfo)) - } - self.notify(event: AdEvent.AdStarted()) + 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()) From d462d611c6eea03885ebc60b8661bd0513bb3bec Mon Sep 17 00:00:00 2001 From: ElizaSapir Date: Thu, 30 Mar 2017 11:03:12 +0300 Subject: [PATCH 12/27] #FEM-1305 #comment remove unsupported API (#129) --- Classes/Player/Player.swift | 11 ++--------- Classes/Player/PlayerController.swift | 8 -------- Classes/Player/PlayerDecoratorBase.swift | 8 -------- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/Classes/Player/Player.swift b/Classes/Player/Player.swift index 57c1138a..abb6b8d1 100644 --- a/Classes/Player/Player.swift +++ b/Classes/Player/Player.swift @@ -66,17 +66,10 @@ import AVKit */ func resume() - func seek(to time: CMTime) - - /** - Prepare for playing the next entry. - */ - func prepareNext(_ config: MediaConfig) -> Bool - /** - Load the entry that was prepared with prepareNext(), without waiting for the current entry to end. + send seek action for the player. */ - func loadNext() -> Bool + func seek(to time: CMTime) /** Release player resources. diff --git a/Classes/Player/PlayerController.swift b/Classes/Player/PlayerController.swift index 2f71b5ae..212bdd86 100644 --- a/Classes/Player/PlayerController.swift +++ b/Classes/Player/PlayerController.swift @@ -95,14 +95,6 @@ class PlayerController: NSObject, Player { self.currentPlayer.currentPosition = CMTimeGetSeconds(time) } - func prepareNext(_ config: MediaConfig) -> Bool { - return false - } - - func loadNext() -> Bool { - return false - } - @available(iOS 9.0, *) func createPiPController(with delegate: AVPictureInPictureControllerDelegate) -> AVPictureInPictureController? { return self.currentPlayer.createPiPController(with: delegate) diff --git a/Classes/Player/PlayerDecoratorBase.swift b/Classes/Player/PlayerDecoratorBase.swift index 5058e003..f5c21d83 100644 --- a/Classes/Player/PlayerDecoratorBase.swift +++ b/Classes/Player/PlayerDecoratorBase.swift @@ -59,14 +59,6 @@ import AVKit public func prepare(_ config: MediaConfig) { return self.player.prepare(config) } - - public func prepareNext(_ config: MediaConfig) -> Bool { - return self.player.prepareNext(config) - } - - public func loadNext() -> Bool { - return self.player.loadNext() - } public func setPlayer(_ player: Player!) { self.player = player From 470971d6de5b5275be10c05416043306c574214c Mon Sep 17 00:00:00 2001 From: ElizaSapir Date: Sat, 1 Apr 2017 22:07:16 +0300 Subject: [PATCH 13/27] expose stop on public API (#130) * expose stop on public API * change log level * remove unrelevant logs --- .../AVPlayerEngine/AVPlayerEngine.swift | 25 ++++++++++++------- Classes/Player/Player.swift | 5 ++++ Classes/Player/PlayerController.swift | 8 +++--- Classes/Player/PlayerDecoratorBase.swift | 4 +++ 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift index 6d62be23..9e6e1781 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift @@ -46,7 +46,7 @@ class AVPlayerEngine: AVPlayer { public var onEventBlock: ((PKEvent) -> Void)? public var view: UIView! { - PKLog.trace("get player view: \(_view)") + PKLog.debug("get player view: \(_view)") return _view } @@ -59,11 +59,11 @@ class AVPlayerEngine: AVPlayer { public var currentPosition: Double { get { - PKLog.trace("get currentPosition: \(self.currentTime())") + PKLog.debug("get currentPosition: \(self.currentTime())") return CMTimeGetSeconds(self.currentTime() - rangeStart) } set { - PKLog.trace("set currentPosition: \(currentPosition)") + PKLog.debug("set currentPosition: \(currentPosition)") let newTime = rangeStart + CMTimeMakeWithSeconds(newValue, 1) super.seek(to: newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) { [unowned self] (isSeeked: Bool) in @@ -100,7 +100,7 @@ class AVPlayerEngine: AVPlayer { } } - PKLog.trace("get duration: \(result)") + PKLog.debug("get duration: \(result)") return result } @@ -178,24 +178,31 @@ class AVPlayerEngine: AVPlayer { self.nonObservablePropertiesUpdateTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.updateNonObservableProperties), userInfo: nil, repeats: true) } - public override func pause() { + func stop() { + PKLog.debug("stop player") + self.pause() + self.seek(to: kCMTimeZero) + self.replaceCurrentItem(with: nil) + } + + override func pause() { if self.rate > 0 { // Playing, so pause. - PKLog.trace("pause player") + PKLog.debug("pause player") super.pause() } } - public override func play() { + override func play() { if self.rate == 0 { - PKLog.trace("play player") + PKLog.debug("play player") self.post(event: PlayerEvent.Play()) super.play() } } func destroy() { - PKLog.trace("destory player") + PKLog.info("destory player") self.nonObservablePropertiesUpdateTimer?.invalidate() self.nonObservablePropertiesUpdateTimer = nil self.removeObservers() diff --git a/Classes/Player/Player.swift b/Classes/Player/Player.swift index abb6b8d1..fdbcb736 100644 --- a/Classes/Player/Player.swift +++ b/Classes/Player/Player.swift @@ -66,6 +66,11 @@ import AVKit */ func resume() + /** + send stop action for the player. + */ + func stop() + /** send seek action for the player. */ diff --git a/Classes/Player/PlayerController.swift b/Classes/Player/PlayerController.swift index 212bdd86..c888681b 100644 --- a/Classes/Player/PlayerController.swift +++ b/Classes/Player/PlayerController.swift @@ -76,22 +76,22 @@ class PlayerController: NSObject, Player { } func play() { - PKLog.trace("play::") self.currentPlayer.play() } func pause() { - PKLog.trace("pause::") self.currentPlayer.pause() } func resume() { - PKLog.trace("resume::") self.currentPlayer.play() } + func stop() { + self.currentPlayer.stop() + } + func seek(to time: CMTime) { - PKLog.trace("seek::\(time)") self.currentPlayer.currentPosition = CMTimeGetSeconds(time) } diff --git a/Classes/Player/PlayerDecoratorBase.swift b/Classes/Player/PlayerDecoratorBase.swift index f5c21d83..efa60d0d 100644 --- a/Classes/Player/PlayerDecoratorBase.swift +++ b/Classes/Player/PlayerDecoratorBase.swift @@ -88,6 +88,10 @@ import AVKit self.player.resume() } + public func stop() { + self.player.stop() + } + @available(iOS 9.0, *) public func createPiPController(with delegate: AVPictureInPictureControllerDelegate) -> AVPictureInPictureController? { return self.player.createPiPController(with: delegate) From efedddb3dba2792a760e6ed9873b69ff11e0d295 Mon Sep 17 00:00:00 2001 From: ElizaSapir Date: Sun, 2 Apr 2017 15:47:50 +0300 Subject: [PATCH 14/27] #FEM-1310 #comment remove onLoad from PlayerLoader and activate onUpdateMedia (#131) --- Classes/Player/PlayerLoader.swift | 2 +- Classes/Plugins/BasePlugin.swift | 4 ---- Classes/Plugins/PKPlugin.swift | 9 +++------ Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift | 7 +------ Plugins/Youbora/YouboraPlugin.swift | 9 ++------- 5 files changed, 7 insertions(+), 24 deletions(-) diff --git a/Classes/Player/PlayerLoader.swift b/Classes/Player/PlayerLoader.swift index 2955ea78..516d827a 100644 --- a/Classes/Player/PlayerLoader.swift +++ b/Classes/Player/PlayerLoader.swift @@ -55,7 +55,7 @@ class PlayerLoader: PlayerDecoratorBase { // update all loaded plugins with media config for (pluginName, loadedPlugin) in loadedPlugins { PKLog.trace("Preparing plugin", pluginName) - loadedPlugin.plugin.onLoad(mediaConfig: config) + loadedPlugin.plugin.onUpdateMedia(mediaConfig: config) } } diff --git a/Classes/Plugins/BasePlugin.swift b/Classes/Plugins/BasePlugin.swift index 67b77a04..60feb130 100644 --- a/Classes/Plugins/BasePlugin.swift +++ b/Classes/Plugins/BasePlugin.swift @@ -25,10 +25,6 @@ import Foundation self.messageBus = messageBus } - @objc public func onLoad(mediaConfig: MediaConfig) { - PKLog.info("plugin \(type(of:self)) onLoad with media config: \(mediaConfig)") - } - @objc public func onUpdateMedia(mediaConfig: MediaConfig) { PKLog.info("plugin \(type(of:self)) onUpdateMedia with media config: \(mediaConfig)") } diff --git a/Classes/Plugins/PKPlugin.swift b/Classes/Plugins/PKPlugin.swift index 0887e201..1a1e6758 100644 --- a/Classes/Plugins/PKPlugin.swift +++ b/Classes/Plugins/PKPlugin.swift @@ -18,14 +18,11 @@ public protocol PKPlugin { weak var player: Player? { get } /// The messageBus associated with the plugin weak var messageBus: MessageBus? { get } - - init(player: Player, pluginConfig: Any?, messageBus: MessageBus) throws - /// On first load. used for doing initialization for the first time with the media config. - func onLoad(mediaConfig: MediaConfig) - /// On update media. used to update the plugin with new media config when available + init(player: Player, pluginConfig: Any?, messageBus: MessageBus) throws + /// On update media. used to update the plugin with new media config when available. func onUpdateMedia(mediaConfig: MediaConfig) - + /// Called on player destroy. func destroy() } diff --git a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift index e79e7c29..8472df0a 100644 --- a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift +++ b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift @@ -18,12 +18,7 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt /************************************************************/ // MARK: - PKPlugin /************************************************************/ - - public override func onLoad(mediaConfig: MediaConfig) { - super.onLoad(mediaConfig: mediaConfig) - AppStateSubject.shared.add(observer: self) - } - + public override func onUpdateMedia(mediaConfig: MediaConfig) { super.onUpdateMedia(mediaConfig: mediaConfig) AppStateSubject.shared.add(observer: self) diff --git a/Plugins/Youbora/YouboraPlugin.swift b/Plugins/Youbora/YouboraPlugin.swift index 2d7cdec2..423df215 100644 --- a/Plugins/Youbora/YouboraPlugin.swift +++ b/Plugins/Youbora/YouboraPlugin.swift @@ -72,8 +72,8 @@ public class YouboraPlugin: BaseAnalyticsPlugin { } } - public override func onLoad(mediaConfig: MediaConfig) { - super.onLoad(mediaConfig: mediaConfig) + public override func onUpdateMedia(mediaConfig: MediaConfig) { + super.onUpdateMedia(mediaConfig: mediaConfig) self.setupYouboraManager() { succeeded in if let player = self.player, succeeded { self.startMonitoring(player: player) @@ -81,11 +81,6 @@ public class YouboraPlugin: BaseAnalyticsPlugin { } } - public override func onUpdateMedia(mediaConfig: MediaConfig) { - super.onUpdateMedia(mediaConfig: mediaConfig) - self.setupYouboraManager() - } - public override func destroy() { super.destroy() self.stopMonitoring() From 341cea72c9fc05f0d9c2ee50ade4153ab0f00d55 Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Sun, 2 Apr 2017 15:48:31 +0300 Subject: [PATCH 15/27] #FEM-1297 (#128) * #FEM-1297 * Separated player error log events from regular errors. Now use errorLog type to observe error log events. Could be helpful for debug and analytics. * Changed player error log code * fixed typo and static analysis issue. --- Classes/PKError.swift | 36 +++++++++++++------ .../AVPlayerEngine+Observation.swift | 4 +-- Classes/PlayerEvent.swift | 12 +++++++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/Classes/PKError.swift b/Classes/PKError.swift index 3e3cb0e5..992849cc 100644 --- a/Classes/PKError.swift +++ b/Classes/PKError.swift @@ -19,7 +19,6 @@ enum PlayerError: PKError { case failedToLoadAssetFromKeys(rootError: NSError?) case assetNotPlayable case failedToPlayToEndTime(rootError: NSError) - case playerItemErrorLogEvent(errorLogEvent: AVPlayerItemErrorLogEvent) static let domain = "com.kaltura.playkit.error.player" @@ -28,7 +27,6 @@ enum PlayerError: PKError { case .failedToLoadAssetFromKeys: return PKErrorCode.failedToLoadAssetFromKeys case .assetNotPlayable: return PKErrorCode.assetNotPlayable case .failedToPlayToEndTime: return PKErrorCode.failedToPlayToEndTime - case .playerItemErrorLogEvent: return PKErrorCode.playerItemErrorLogEvent } } @@ -37,7 +35,6 @@ enum PlayerError: PKError { 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 .playerItemErrorLogEvent(let errorLogEvent): return errorLogEvent.errorComment ?? "" } } @@ -50,15 +47,33 @@ enum PlayerError: PKError { return [:] case .assetNotPlayable: return [:] case .failedToPlayToEndTime(let rootError): return [PKErrorKeys.RootErrorKey: rootError] - case .playerItemErrorLogEvent(let errorLogEvent): - return [ - PKErrorKeys.RootCodeKey: errorLogEvent.errorStatusCode, - PKErrorKeys.RootDomainKey: errorLogEvent.errorDomain - ] } } } +/// `PlayerErrorLog` represents an error log emitted from AVPlayer (usually non-fatal). +struct PlayerErrorLog: PKError { + + static var domain = PlayerError.domain + + let errorLogEvent: AVPlayerItemErrorLogEvent + + var code: Int { + return PKErrorCode.playerItemErrorLogEvent + } + + var errorDescription: String { + return errorLogEvent.errorComment ?? "" + } + + var userInfo: [String: Any] { + return [ + PKErrorKeys.RootCodeKey: errorLogEvent.errorStatusCode, + PKErrorKeys.RootDomainKey: errorLogEvent.errorDomain + ] + } +} + /************************************************************/ // MARK: - PKPluginError /************************************************************/ @@ -111,7 +126,7 @@ protocol PKError: Error, CustomStringConvertible { /** The error code. - use `switch self` to retrive the value in **enums**. + use `switch self` to retrieve the value in **enums**. ```` var code: Int { @@ -222,7 +237,8 @@ struct PKErrorKeys { @objc(FailedToLoadAssetFromKeys) public static let failedToLoadAssetFromKeys = 7000 @objc(AssetNotPlayable) public static let assetNotPlayable = 7001 @objc(FailedToPlayToEndTime) public static let failedToPlayToEndTime = 7002 - @objc(PlayerItemErrorLogEvent) public static let playerItemErrorLogEvent = 7003 + // PlayerErrorLog + @objc(PlayerItemErrorLogEvent) public static let playerItemErrorLogEvent = 7100 // PKPluginError @objc(FailedToCreatePlugin) public static let failedToCreatePlugin = 2000 @objc(MissingPluginConfig) public static let missingPluginConfig = 2001 diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift index b3b9bae3..00ec430f 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift @@ -75,8 +75,8 @@ extension AVPlayerEngine { func onErrorLogEntryNotification(notification: Notification) { guard let playerItem = notification.object as? AVPlayerItem, let errorLog = playerItem.errorLog(), let lastEvent = errorLog.events.last else { return } - PKLog.error("error description: \(lastEvent.errorComment), error domain: \(lastEvent.errorDomain), error code: \(lastEvent.errorStatusCode)") - self.post(event: PlayerEvent.Error(error: PlayerError.playerItemErrorLogEvent(errorLogEvent: lastEvent))) + PKLog.warning("error description: \(String(describing: lastEvent.errorComment)), error domain: \(lastEvent.errorDomain), error code: \(lastEvent.errorStatusCode)") + self.post(event: PlayerEvent.ErrorLog(error: PlayerErrorLog(errorLogEvent: lastEvent))) } public func playerFailed(notification: NSNotification) { diff --git a/Classes/PlayerEvent.swift b/Classes/PlayerEvent.swift index 835d5bac..5fabc077 100644 --- a/Classes/PlayerEvent.swift +++ b/Classes/PlayerEvent.swift @@ -51,6 +51,8 @@ import AVFoundation @objc public static let error: PlayerEvent.Type = Error.self /// Sent when an plugin error occurs. @objc public static let pluginError: PlayerEvent.Type = PluginError.self + /// Sent when an error log event received from player. + @objc public static let errorLog: PlayerEvent.Type = ErrorLog.self // MARK: - Player Basic Events @@ -89,6 +91,16 @@ import AVFoundation } } + class ErrorLog: PlayerEvent { + convenience init(nsError: NSError) { + self.init([EventDataKeys.Error: nsError]) + } + + convenience init(error: PKError) { + self.init([EventDataKeys.Error: error.asNSError]) + } + } + class TimedMetadata: PlayerEvent { convenience init(metadata: [AVMetadataItem]) { self.init([EventDataKeys.Metadata: metadata]) From f503c4ee1d1750965804e32eb0cad4961b6bc272 Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Mon, 3 Apr 2017 09:39:17 +0300 Subject: [PATCH 16/27] FEM-1302 (#132) * #FEM-1302 * Added timebase event to post `playing` events. * * Moved pause event posting to timebase observing for more accurate results. Also, now we can remove isPlayedToEndTime flag. * * fixed small issue with accessing player.rate from background thread. Now accessing on the main thread. --- Classes/MessageBus.swift | 2 +- .../AVPlayerEngine+Observation.swift | 32 +++++++------- .../AVPlayerEngine/AVPlayerEngine.swift | 42 +++---------------- 3 files changed, 24 insertions(+), 52 deletions(-) diff --git a/Classes/MessageBus.swift b/Classes/MessageBus.swift index 7a16488a..d91e0ffe 100644 --- a/Classes/MessageBus.swift +++ b/Classes/MessageBus.swift @@ -55,7 +55,7 @@ private struct Observation { @objc public func post(_ event: PKEvent) { DispatchQueue.global().async { [weak self] in - PKLog.info("Post event: \(event)") + PKLog.info("Post event: \(String(describing: type(of: event)))") let typeId = NSStringFromClass(type(of: event)) if let array = self?.observations[typeId] { diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift index 00ec430f..2c79d4b1 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift @@ -39,6 +39,7 @@ extension AVPlayerEngine { NotificationCenter.default.addObserver(self, selector: #selector(self.playerPlayedToEnd(notification:)), name: .AVPlayerItemDidPlayToEndTime, object: self.currentItem) 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) } func removeObservers() { @@ -57,6 +58,7 @@ extension AVPlayerEngine { NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) NotificationCenter.default.removeObserver(self, name: .AVPlayerItemNewAccessLogEntry, object: nil) NotificationCenter.default.removeObserver(self, name: .AVPlayerItemNewErrorLogEntry, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name(kCMTimebaseNotification_EffectiveRateChanged as String), object: nil) } func onAccessLogEntryNotification(notification: Notification) { @@ -95,10 +97,8 @@ extension AVPlayerEngine { let newState = PlayerState.idle self.postStateChange(newState: newState, oldState: self.currentState) self.currentState = newState - self.isPlayedToEndTime = true // In iOS 9 and below rate is 1.0 even when playback is finished. // To make sure rate will be 0.0 (paused) when played to end we call pause manually. - // calling pause after `isPlayedToEndTime` will make sure no pause event will be sent in messageBus. self.pause() // pause should be called before ended to make sure our rate will be 0.0 when ended event will be observed. self.post(event: PlayerEvent.Ended()) @@ -157,21 +157,27 @@ extension AVPlayerEngine { } } - /// Handles change in player rate - /// - /// - Returns: The event to post, rate <= 0 means pause event. - private func handleRate() { - if rate > 0 { - self.startOrResumeNonObservablePropertiesUpdateTimer() - } else { - self.nonObservablePropertiesUpdateTimer?.invalidate() - // we don't want pause events to be sent when current item reached end. - if !isPlayedToEndTime { + /// Handles changes in player timebase + func timebaseChanged(notification: Notification) { + // for some reason timebase rate changed is received on a background thread. + // in order to check self.rate we must make sure we are on the main thread. + DispatchQueue.main.sync { + guard let timebase = self.currentItem?.timebase else { return } + PKLog.debug("timebase changed, current timebase: \(String(describing: timebase))") + let timebaseRate = CMTimebaseGetRate(timebase) + if timebaseRate > 0 { + self.post(event: PlayerEvent.Playing()) + } else if timebaseRate == 0 && self.rate == 0 { self.post(event: PlayerEvent.Pause()) } } } + /// Handles changes in player rate + private func handleRate() { + PKLog.debug("player rate was changed, now: \(self.rate)") + } + private func handleStatusChange() { if currentItem?.status == .readyToPlay { let newState = PlayerState.ready @@ -201,8 +207,6 @@ extension AVPlayerEngine { let newState = PlayerState.idle self.postStateChange(newState: newState, oldState: self.currentState) self.currentState = newState - // in case item changed reset player reached end time indicator - self.isPlayedToEndTime = false } private func handleTimedMedia() { diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift index 9e6e1781..51769b19 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift @@ -32,15 +32,6 @@ class AVPlayerEngine: AVPlayer { var isObserved: Bool = false var currentState: PlayerState = PlayerState.idle var tracksManager = TracksManager() - - /// Indicates whether the current items was played until the end. - /// - /// - note: Used for preventing 'pause' events to be sent after 'ended' event. - var isPlayedToEndTime: Bool = false - - // AVPlayerItem.currentTime() and the AVPlayerItem.timebase's rate are not KVO observable. We check their values regularly using this timer. - var nonObservablePropertiesUpdateTimer: Timer? - var observerContext = 0 public var onEventBlock: ((PKEvent) -> Void)? @@ -59,24 +50,20 @@ class AVPlayerEngine: AVPlayer { public var currentPosition: Double { get { - PKLog.debug("get currentPosition: \(self.currentTime())") + PKLog.trace("get currentPosition: \(self.currentTime())") return CMTimeGetSeconds(self.currentTime() - rangeStart) } set { PKLog.debug("set currentPosition: \(currentPosition)") - let newTime = rangeStart + CMTimeMakeWithSeconds(newValue, 1) super.seek(to: newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) { [unowned self] (isSeeked: Bool) in if isSeeked { - // when seeked successfully reset player reached end time indicator - self.isPlayedToEndTime = false self.post(event: PlayerEvent.Seeked()) PKLog.debug("seeked") } else { PKLog.error("seek faild") } } - self.post(event: PlayerEvent.Seeking()) } } @@ -100,7 +87,7 @@ class AVPlayerEngine: AVPlayer { } } - PKLog.debug("get duration: \(result)") + PKLog.trace("get duration: \(result)") return result } @@ -162,7 +149,6 @@ class AVPlayerEngine: AVPlayer { _view = PlayerView(playerLayer: avPlayerLayer) self.onEventBlock = nil - self.nonObservablePropertiesUpdateTimer = nil AppStateSubject.shared.add(observer: self) } @@ -173,13 +159,8 @@ class AVPlayerEngine: AVPlayer { } } - func startOrResumeNonObservablePropertiesUpdateTimer() { - PKLog.debug("setupNonObservablePropertiesUpdateTimer") - self.nonObservablePropertiesUpdateTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.updateNonObservableProperties), userInfo: nil, repeats: true) - } - func stop() { - PKLog.debug("stop player") + PKLog.info("stop player") self.pause() self.seek(to: kCMTimeZero) self.replaceCurrentItem(with: nil) @@ -202,9 +183,7 @@ class AVPlayerEngine: AVPlayer { } func destroy() { - PKLog.info("destory player") - self.nonObservablePropertiesUpdateTimer?.invalidate() - self.nonObservablePropertiesUpdateTimer = nil + PKLog.info("destroy player") self.removeObservers() self.avPlayerLayer = nil self._view = nil @@ -231,7 +210,7 @@ class AVPlayerEngine: AVPlayer { } func post(event: PKEvent) { - PKLog.debug("onEvent:: \(event)") + PKLog.trace("onEvent:: \(event)") onEventBlock?(event) } @@ -240,17 +219,6 @@ class AVPlayerEngine: AVPlayer { let stateChangedEvent: PKEvent = PlayerEvent.StateChanged(newState: newState, oldState: oldState) self.post(event: stateChangedEvent) } - - // MARK: - Non Observable Properties - @objc func updateNonObservableProperties() { - guard let timebase = self.currentItem?.timebase else { return } - let timebaseRate = CMTimebaseGetRate(timebase) - if timebaseRate > 0 { - self.nonObservablePropertiesUpdateTimer?.invalidate() - self.post(event: PlayerEvent.Playing()) - } - PKLog.debug("timebaseRate:: \(timebaseRate)") - } } /************************************************************/ From a70e6b49cdb001c6408f7a1db4729ebef84e516a Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Mon, 3 Apr 2017 13:45:32 +0300 Subject: [PATCH 17/27] In addition to FEM-1302, added a check to compare last timebase rate, fixes an issue. (#133) --- .../AVPlayerEngine/AVPlayerEngine+Observation.swift | 10 ++++++---- Classes/Player/AVPlayerEngine/AVPlayerEngine.swift | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift index 2c79d4b1..b9fc7236 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift @@ -161,15 +161,17 @@ extension AVPlayerEngine { func timebaseChanged(notification: Notification) { // for some reason timebase rate changed is received on a background thread. // in order to check self.rate we must make sure we are on the main thread. - DispatchQueue.main.sync { + DispatchQueue.main.async { guard let timebase = self.currentItem?.timebase else { return } - PKLog.debug("timebase changed, current timebase: \(String(describing: timebase))") + PKLog.trace("timebase changed, current timebase: \(String(describing: timebase))") let timebaseRate = CMTimebaseGetRate(timebase) - if timebaseRate > 0 { + if timebaseRate > 0 && self.lastTimebaseRate != timebaseRate { self.post(event: PlayerEvent.Playing()) - } else if timebaseRate == 0 && self.rate == 0 { + } else if timebaseRate == 0 && self.rate == 0 && self.lastTimebaseRate != timebaseRate { self.post(event: PlayerEvent.Pause()) } + // make sure to save the last value so we could only post events only when currentTimebase != lastTimebase + self.lastTimebaseRate = timebaseRate } } diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift index 51769b19..5583f6e7 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift @@ -28,6 +28,7 @@ class AVPlayerEngine: AVPlayer { private var _view: PlayerView! private var isDestroyed = false + var lastTimebaseRate: Float64 = 0 var lastBitrate: Double = 0 var isObserved: Bool = false var currentState: PlayerState = PlayerState.idle From dcf4009de6f3b27b3670285ce7cb0c11df0c36a6 Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Tue, 4 Apr 2017 09:43:15 +0300 Subject: [PATCH 18/27] #FEM-1314 (#134) * Added `isFirstReady` flag to indicate first time the player was in ready to play state. This helps fix an issue where can play event would be posted numerous times. --- .../AVPlayerEngine/AVPlayerEngine+AssetLoading.swift | 3 +++ .../Player/AVPlayerEngine/AVPlayerEngine+Observation.swift | 7 +++++-- Classes/Player/AVPlayerEngine/AVPlayerEngine.swift | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift index b0dcd54e..296c1c25 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine+AssetLoading.swift @@ -60,6 +60,9 @@ 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 it our player's current item. diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift index b9fc7236..a050e552 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine+Observation.swift @@ -183,7 +183,6 @@ extension AVPlayerEngine { private func handleStatusChange() { if currentItem?.status == .readyToPlay { let newState = PlayerState.ready - self.post(event: PlayerEvent.LoadedMetadata()) if self.startPosition > 0 { self.currentPosition = self.startPosition @@ -197,7 +196,11 @@ extension AVPlayerEngine { self.postStateChange(newState: newState, oldState: self.currentState) self.currentState = newState - self.post(event: PlayerEvent.CanPlay()) + if self.isFirstReady { + self.isFirstReady = false + self.post(event: PlayerEvent.LoadedMetadata()) + self.post(event: PlayerEvent.CanPlay()) + } } else if currentItem?.status == .failed { let newState = PlayerState.error self.postStateChange(newState: newState, oldState: self.currentState) diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift index 5583f6e7..df60286f 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift @@ -27,10 +27,13 @@ class AVPlayerEngine: AVPlayer { private var avPlayerLayer: AVPlayerLayer! private var _view: PlayerView! private var isDestroyed = false - + /// Keeps reference on the last timebase rate in order to post events accuratly. var lastTimebaseRate: Float64 = 0 var lastBitrate: Double = 0 var isObserved: Bool = false + /// Indicates if player item was changed to state: `readyToPlay` at least once. + /// Used to post `CanPlay` event once on first `readyToPlay`. + var isFirstReady = true var currentState: PlayerState = PlayerState.idle var tracksManager = TracksManager() var observerContext = 0 From d3df037bd5ac4fad9790bf3f57dda69a6634e3ed Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Tue, 4 Apr 2017 18:01:15 +0300 Subject: [PATCH 19/27] General improvements (#135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * * Added protection for play, pause and destroy to be called on main thread to make sure no unpredictable behavior happens. * * Added `DispatchQueue.main.async` calls to play, pause and destroy functions to make sure they are dealt with on main thread. * Removed `isDestroyed` flag from `AVPlayerEngine` wasn’t necessary. * Added comments and improved some logs. * * Added fix to `AppStateSubject` observation. --- .../Managers/AppState/AppStateSubject.swift | 31 ++++++++--- Classes/MessageBus.swift | 8 ++- .../AVPlayerEngine/AVPlayerEngine.swift | 51 +++++++++++-------- Classes/Player/PlayerController.swift | 6 ++- 4 files changed, 62 insertions(+), 34 deletions(-) diff --git a/Classes/Managers/AppState/AppStateSubject.swift b/Classes/Managers/AppState/AppStateSubject.swift index 7fb15194..d67129a7 100644 --- a/Classes/Managers/AppState/AppStateSubject.swift +++ b/Classes/Managers/AppState/AppStateSubject.swift @@ -17,7 +17,7 @@ protocol AppStateSubjectProtocol: class, AppStateProviderDelegate { /// The app state events provider. var appStateProvider: AppStateProvider { get } /// The current app state observers. - var observers: [AppStateObservable] { get set } + var observers: [AppStateObserver] { get set } /// States whether currently observing. /// - note: when mocking set initial value to false. var isObserving: Bool { get set } @@ -50,20 +50,23 @@ extension AppStateSubjectProtocol { /// Adds an observer to inform when state events are posted. func add(observer: AppStateObservable) { sync { + cleanObservers() PKLog.trace("add observer, \(observer)") // if no observers were available start observing now if observers.count == 0 && !isObserving { startObservingAppState() } - observers.append(observer) + observers.append(AppStateObserver(observer)) } } /// Removes an observer to stop being inform when state events are posted. func remove(observer: AppStateObservable) { sync { + cleanObservers() + // search for the observer to remove for i in 0.. Bool { return lhs.name.rawValue == rhs.name.rawValue } +class AppStateObserver: AnyObject { + weak var observer: AppStateObservable? + init(_ observer: AppStateObservable) { + self.observer = observer + } +} + /// A type that provides a set of NotificationObservation to observe. /// This interface defines the observations we would want in our class, for example a set of [willTerminate, didEnterBackground etc.] protocol AppStateObservable: AnyObject { diff --git a/Classes/MessageBus.swift b/Classes/MessageBus.swift index d91e0ffe..29c10951 100644 --- a/Classes/MessageBus.swift +++ b/Classes/MessageBus.swift @@ -9,11 +9,15 @@ import Foundation private struct Observation { + /// the observer of the event weak var observer: AnyObject? + /// the dispatchQueue to observe the events on. let observeOn: DispatchQueue + /// the block of code to be performed when event fires. let block: (PKEvent) -> Void } +/// `MessageBus` object handles all event message observing and posting @objc public class MessageBus: NSObject { private var observations = [String: [Observation]]() private let lock: AnyObject = UUID().uuidString as AnyObject @@ -28,7 +32,7 @@ private struct Observation { private func add(observer: AnyObject, events: [PKEvent.Type], observeOn dispatchQueue: DispatchQueue = DispatchQueue.main, block: @escaping (PKEvent)->Void) { sync { - PKLog.debug("Add observer: \(observer) for events: \(events)") + PKLog.debug("Add observer: \(String(describing: observer)) for events: \(String(describing: events))") events.forEach { (et) in let typeId = NSStringFromClass(et) var observationList: [Observation] = observations[typeId] ?? [] @@ -40,7 +44,7 @@ private struct Observation { @objc public func removeObserver(_ observer: AnyObject, events: [PKEvent.Type]) { sync { - PKLog.debug("Remove observer: \(observer) for events: \(events)") + PKLog.debug("Remove observer: \(String(describing: observer)) for events: \(String(describing: events))") events.forEach { (et) in let typeId = NSStringFromClass(et) diff --git a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift index df60286f..c694a23b 100644 --- a/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift +++ b/Classes/Player/AVPlayerEngine/AVPlayerEngine.swift @@ -26,7 +26,7 @@ class AVPlayerEngine: AVPlayer { private var avPlayerLayer: AVPlayerLayer! private var _view: PlayerView! - private var isDestroyed = false + /// Keeps reference on the last timebase rate in order to post events accuratly. var lastTimebaseRate: Float64 = 0 var lastBitrate: Double = 0 @@ -158,9 +158,7 @@ class AVPlayerEngine: AVPlayer { } deinit { - if !isDestroyed { - self.destroy() - } + PKLog.debug("\(String(describing: type(of: self))), was deinitialized") } func stop() { @@ -171,31 +169,40 @@ class AVPlayerEngine: AVPlayer { } override func pause() { - if self.rate > 0 { - // Playing, so pause. - PKLog.debug("pause player") - super.pause() + // makes sure play/pause call is made on the main thread (calling on background thread has unpredictable behaviours) + DispatchQueue.main.async { + if self.rate > 0 { + // Playing, so pause. + PKLog.debug("pause player") + super.pause() + } } } override func play() { - if self.rate == 0 { - PKLog.debug("play player") - self.post(event: PlayerEvent.Play()) - super.play() + // makes sure play/pause call is made on the main thread (calling on background thread has unpredictable behaviours) + DispatchQueue.main.async { + if self.rate == 0 { + PKLog.debug("play player") + self.post(event: PlayerEvent.Play()) + super.play() + } } } func destroy() { - PKLog.info("destroy player") - self.removeObservers() - self.avPlayerLayer = nil - self._view = nil - self.onEventBlock = nil - // removes app state observer - AppStateSubject.shared.remove(observer: self) - self.replaceCurrentItem(with: nil) - self.isDestroyed = true + // make sure to call destroy on main thread synchronously. + // this make sure everything will be cleared without any race conditions + DispatchQueue.main.async { + PKLog.info("destroy player") + self.removeObservers() + self.avPlayerLayer = nil + self._view = nil + self.onEventBlock = nil + // removes app state observer + AppStateSubject.shared.remove(observer: self) + self.replaceCurrentItem(with: nil) + } } @available(iOS 9.0, *) @@ -214,7 +221,7 @@ class AVPlayerEngine: AVPlayer { } func post(event: PKEvent) { - PKLog.trace("onEvent:: \(event)") + PKLog.trace("onEvent:: \(String(describing: event))") onEventBlock?(event) } diff --git a/Classes/Player/PlayerController.swift b/Classes/Player/PlayerController.swift index c888681b..2267ea63 100644 --- a/Classes/Player/PlayerController.swift +++ b/Classes/Player/PlayerController.swift @@ -121,10 +121,12 @@ class PlayerController: NSObject, Player { /************************************************************/ // MARK: - Reachability & Application States Handling /************************************************************/ + extension PlayerController { + private func shouldRefreshAsset() { if let handler = self.assetBuilder?.assetHandler as? RefreshableAssetHandler { - if let (source, handlerClass) = self.assetBuilder!.getPreferredMediaSource() { + if let (source, _) = self.assetBuilder!.getPreferredMediaSource() { handler.shouldRefreshAsset(mediaSource: source) { [unowned self] (shouldRefresh) in if shouldRefresh { self.shouldRefresh = true @@ -137,7 +139,7 @@ extension PlayerController { private func refreshAsset() { if let handler = self.assetBuilder?.assetHandler as? RefreshableAssetHandler { - if let (source, handlerClass) = self.assetBuilder!.getPreferredMediaSource() { + if let (source, _) = self.assetBuilder!.getPreferredMediaSource() { self.currentPlayer.startPosition = self.currentPlayer.currentPosition handler.refreshAsset(mediaSource: source) } From cfe23cf3708d997dc986b796b74b5657ab2401e4 Mon Sep 17 00:00:00 2001 From: ElizaSapir Date: Tue, 4 Apr 2017 18:45:07 +0300 Subject: [PATCH 20/27] Fem 1311 (#136) * FEM-1311 #comment updatePluginConfig attachment * support change media on IMA plugin * typo fix --- Classes/Player/Player.swift | 97 +++++++++----------- Classes/Player/PlayerController.swift | 4 + Classes/Player/PlayerDecoratorBase.swift | 4 + Classes/Player/PlayerLoader.swift | 9 ++ Classes/Plugins/BasePlugin.swift | 6 +- Classes/Plugins/PKPlugin.swift | 2 + Plugins/IMA/AdsEnabledPlayerController.swift | 15 ++- Plugins/IMA/AdsPlugin.swift | 1 + Plugins/IMA/IMAPlugin.swift | 41 ++++++--- 9 files changed, 110 insertions(+), 69 deletions(-) diff --git a/Classes/Player/Player.swift b/Classes/Player/Player.swift index fdbcb736..0b7b34db 100644 --- a/Classes/Player/Player.swift +++ b/Classes/Player/Player.swift @@ -19,79 +19,66 @@ import AVKit @objc weak var delegate: PlayerDelegate? { get set } /// The player's associated media entry. - weak var mediaEntry: MediaEntry? { get } + @objc weak var mediaEntry: MediaEntry? { get } - /** - Get the player's layer component. - */ - var view: UIView! { get } + /// Get the player's layer component. + @objc var view: UIView! { get } - /** - Get/set the current player position. - */ - var currentTime: TimeInterval { get set } - - /** - Get the player's duration. - */ - var isPlaying: Bool { get } - - /** - Get the player's duration. - */ - var duration: TimeInterval { get } + /// Get/set the current player position. + @objc var currentTime: TimeInterval { get set } + + /// Get the player's duration. + @objc var isPlaying: Bool { get } + + /// Get the player's duration. + @objc var duration: TimeInterval { get } - var currentAudioTrack: String? { get } + /// Get the player's current audio track. + @objc var currentAudioTrack: String? { get } - var currentTextTrack: String? { get } + /// Get the player's current text track. + @objc var currentTextTrack: String? { get } - /** - Prepare for playing an entry. - play when it's ready. - */ - func prepare(_ config: MediaConfig) + /// Prepare for playing an entry. + /// play when it's ready. + @objc func prepare(_ config: MediaConfig) - /** - send play action for the player. - */ - func play() + /// send play action for the player. + @objc func play() - /** - send pause action for the player. - */ - func pause() + /// send pause action for the player. + @objc func pause() - /** - send resume action for the player. - */ - func resume() + /// send resume action for the player. + @objc func resume() - /** - send stop action for the player. - */ - func stop() + /// send stop action for the player. + @objc func stop() - /** - send seek action for the player. - */ - func seek(to time: CMTime) + /// send seek action for the player. + @objc func seek(to time: CMTime) + + /// Release player resources. + @objc func destroy() - /** - Release player resources. - */ - func destroy() + /// Add Observation to relevant event. + @objc func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (PKEvent)->Void) - func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (PKEvent)->Void) + /// Remove Observation. + @objc func removeObserver(_ observer: AnyObject, events: [PKEvent.Type]) - func removeObserver(_ observer: AnyObject, events: [PKEvent.Type]) + /// Select Track + @objc func selectTrack(trackId: String) - func selectTrack(trackId: String) + /// Update Plugin Config + @objc func updatePluginConfig(pluginName: String, config: Any) + /// Create PiP Controller @available(iOS 9.0, *) - func createPiPController(with delegate: AVPictureInPictureControllerDelegate) -> AVPictureInPictureController? + @objc func createPiPController(with delegate: AVPictureInPictureControllerDelegate) -> AVPictureInPictureController? } -protocol PlayerDecoratorProvider: NSObjectProtocol { +public protocol PlayerDecoratorProvider { func getPlayerDecorator() -> PlayerDecoratorBase? } diff --git a/Classes/Player/PlayerController.swift b/Classes/Player/PlayerController.swift index 2267ea63..7b134bf0 100644 --- a/Classes/Player/PlayerController.swift +++ b/Classes/Player/PlayerController.swift @@ -116,6 +116,10 @@ class PlayerController: NSObject, Player { public func selectTrack(trackId: String) { self.currentPlayer.selectTrack(trackId: trackId) } + + public func updatePluginConfig(pluginName: String, config: Any) { + //Assert.shouldNeverHappen(); + } } /************************************************************/ diff --git a/Classes/Player/PlayerDecoratorBase.swift b/Classes/Player/PlayerDecoratorBase.swift index efa60d0d..6dc625b2 100644 --- a/Classes/Player/PlayerDecoratorBase.swift +++ b/Classes/Player/PlayerDecoratorBase.swift @@ -97,6 +97,10 @@ import AVKit return self.player.createPiPController(with: delegate) } + public func updatePluginConfig(pluginName: String, config: Any) { + self.player.updatePluginConfig(pluginName: pluginName, config: config) + } + public func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (PKEvent) -> Void) { //Assert.shouldNeverHappen(); } diff --git a/Classes/Player/PlayerLoader.swift b/Classes/Player/PlayerLoader.swift index 516d827a..d5578e40 100644 --- a/Classes/Player/PlayerLoader.swift +++ b/Classes/Player/PlayerLoader.swift @@ -98,4 +98,13 @@ class PlayerLoader: PlayerDecoratorBase { messageBus.removeObserver(observer, events: events) } + public override func updatePluginConfig(pluginName: String, config: Any) { + guard let loadedPlugin: LoadedPlugin = loadedPlugins[pluginName] else { + PKLog.debug("There is no such plugin: \(pluginName)"); + return + } + + loadedPlugin.plugin.onUpdateConfig(pluginConfig: config) + } + } diff --git a/Classes/Plugins/BasePlugin.swift b/Classes/Plugins/BasePlugin.swift index 60feb130..70ac91b5 100644 --- a/Classes/Plugins/BasePlugin.swift +++ b/Classes/Plugins/BasePlugin.swift @@ -26,7 +26,11 @@ import Foundation } @objc public func onUpdateMedia(mediaConfig: MediaConfig) { - PKLog.info("plugin \(type(of:self)) onUpdateMedia with media config: \(mediaConfig)") + PKLog.info("plugin \(type(of:self)) onUpdateMedia with media config: \(String(describing: mediaConfig))") + } + + @objc public func onUpdateConfig(pluginConfig: Any) { + PKLog.info("plugin \(type(of:self)) onUpdateConfig with media config: \(String(describing: pluginConfig))") } @objc public func destroy() { diff --git a/Classes/Plugins/PKPlugin.swift b/Classes/Plugins/PKPlugin.swift index 1a1e6758..40553693 100644 --- a/Classes/Plugins/PKPlugin.swift +++ b/Classes/Plugins/PKPlugin.swift @@ -22,6 +22,8 @@ public protocol PKPlugin { init(player: Player, pluginConfig: Any?, messageBus: MessageBus) throws /// On update media. used to update the plugin with new media config when available. func onUpdateMedia(mediaConfig: MediaConfig) + /// On update config. used to update the plugin config. + func onUpdateConfig(pluginConfig: Any) /// Called on player destroy. func destroy() } diff --git a/Plugins/IMA/AdsEnabledPlayerController.swift b/Plugins/IMA/AdsEnabledPlayerController.swift index 77c2e286..1ec235e3 100644 --- a/Plugins/IMA/AdsEnabledPlayerController.swift +++ b/Plugins/IMA/AdsEnabledPlayerController.swift @@ -32,7 +32,6 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl didSet { self.adsPlugin.delegate = self self.adsPlugin.dataSource = self - self.adsPlugin.requestAds() } } @@ -70,6 +69,20 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl } } + override func stop() { + self.adsPlugin.destroyManager() + super.stop() + self.isAdPlayback = false + 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 diff --git a/Plugins/IMA/AdsPlugin.swift b/Plugins/IMA/AdsPlugin.swift index 2d2b22d6..2583bf49 100644 --- a/Plugins/IMA/AdsPlugin.swift +++ b/Plugins/IMA/AdsPlugin.swift @@ -29,5 +29,6 @@ protocol AdsPlugin: PKPlugin, AVPictureInPictureControllerDelegate { func resume() func pause() func contentComplete() + func destroyManager() } diff --git a/Plugins/IMA/IMAPlugin.swift b/Plugins/IMA/IMAPlugin.swift index 29e782c0..539215af 100644 --- a/Plugins/IMA/IMAPlugin.swift +++ b/Plugins/IMA/IMAPlugin.swift @@ -51,7 +51,6 @@ extension PKAdInfo { private var loadingView: UIView? // we must have config error will be thrown otherwise private var config: AdsConfig! - private var adTagUrl: String! private var isAdPlayback = false private var startAdCalled = false @@ -97,10 +96,6 @@ extension PKAdInfo { IMAPlugin.loader.contentComplete() IMAPlugin.loader.delegate = self - - if let adTagUrl = adsConfig.adTagUrl { - self.adTagUrl = adTagUrl - } } else { PKLog.error("missing plugin config") throw PKPluginError.missingPluginConfig(pluginName: IMAPlugin.pluginName) @@ -111,6 +106,22 @@ extension PKAdInfo { } } + public override func onUpdateConfig(pluginConfig: Any) { + PKLog.debug("pluginConfig: " + String(describing: pluginConfig)) + + super.onUpdateConfig(pluginConfig: pluginConfig) + + if let adsConfig = pluginConfig as? AdsConfig { + 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.destroyManager() @@ -120,7 +131,7 @@ extension PKAdInfo { // MARK: - PlayerDecoratorProvider /************************************************************/ - func getPlayerDecorator() -> PlayerDecoratorBase? { + public func getPlayerDecorator() -> PlayerDecoratorBase? { return AdsEnabledPlayerController(adsPlugin: self) } @@ -131,7 +142,7 @@ extension PKAdInfo { func requestAds() { guard let playerView = player?.view else { return } - if self.adTagUrl != nil && self.adTagUrl != "" { + 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. @@ -139,13 +150,13 @@ extension PKAdInfo { 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]) + adDisplayContainer = IMAAdDisplayContainer(adContainer: playerView, companionSlots: [companionAdSlot!]) } else { adDisplayContainer = IMAAdDisplayContainer(adContainer: playerView, companionSlots: []) } var request: IMAAdsRequest - request = IMAAdsRequest(adTagUrl: self.adTagUrl, adDisplayContainer: adDisplayContainer, contentPlayhead: self, userContext: nil) + request = IMAAdsRequest(adTagUrl: self.config.adTagUrl, adDisplayContainer: adDisplayContainer, contentPlayhead: self, userContext: nil) IMAPlugin.loader.requestAds(with: request) PKLog.trace("request Ads") @@ -158,7 +169,7 @@ extension PKAdInfo { return false } - if self.adTagUrl != nil && self.adTagUrl != "" { + if self.config.adTagUrl != nil && self.config.adTagUrl != "" { if showLoadingView { self.showLoadingView(true, alpha: 1) } @@ -254,14 +265,20 @@ 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.adTagUrl != nil && adCuePoints.count > 0 { + if self.config.adTagUrl != nil && adCuePoints.count > 0 { self.notify(event: AdEvent.AdCuePointsUpdate(adCuePoints: adCuePoints)) } } - private func destroyManager() { + 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 } From 7952f1577f5dee09ec7d4b934c5b5f32ce4a3f76 Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Thu, 6 Apr 2017 14:38:32 +0300 Subject: [PATCH 21/27] FEM-1320 (#138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #FEM-1320 * Added `PlayerSettings` to provide the ability to change our content request adapter. * Added `KalturaPlaybackRequestAdapter` providing adaptation of the content request if we have ‘playManifest’ inside, to add uuid and clientTag params to the request. * * Fixed issue where content request adapter couldn’t be removed. --- .../KalturaPlaybackRequestAdapter.swift | 42 ++++++++++ Classes/PKRequestParams.swift | 26 +++++++ Classes/Player/AssetBuilder.swift | 6 +- Classes/Player/DefaultAssetHandler.swift | 8 +- Classes/Player/MediaEntry.swift | 78 +++++++++---------- Classes/Player/Player.swift | 30 ++++--- Classes/Player/PlayerController.swift | 23 ++++++ Classes/Player/PlayerDecoratorBase.swift | 24 +++++- 8 files changed, 176 insertions(+), 61 deletions(-) create mode 100644 Classes/Network/KalturaPlaybackRequestAdapter.swift create mode 100644 Classes/PKRequestParams.swift diff --git a/Classes/Network/KalturaPlaybackRequestAdapter.swift b/Classes/Network/KalturaPlaybackRequestAdapter.swift new file mode 100644 index 00000000..57ebe68d --- /dev/null +++ b/Classes/Network/KalturaPlaybackRequestAdapter.swift @@ -0,0 +1,42 @@ +// +// KalturaPlaybackRequestAdapter.swift +// Pods +// +// Created by Gal Orlanczyk on 05/04/2017. +// +// + +import Foundation + +class KalturaPlaybackRequestAdapter: PKRequestParamsAdapter { + + private var playSessionId: UUID + + init(playSessionId: UUID) { + self.playSessionId = playSessionId + } + + public static func setup(player: Player) { + let adapter = KalturaPlaybackRequestAdapter(playSessionId: player.sessionId) + player.settings.set(contentRequestAdapter: adapter) + } + + public func adapt(requestParams: PKRequestParams) -> PKRequestParams { + 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)] + if var urlQueryItems = urlComponents.queryItems { + urlQueryItems += queryItems + urlComponents.queryItems = urlQueryItems + } else { + urlComponents.queryItems = queryItems + } + // create the url + guard let url = urlComponents.url else { + PKLog.debug("failed to create url after appending query items") + return requestParams + } + return PKRequestParams(url: url, headers: requestParams.headers) + } +} diff --git a/Classes/PKRequestParams.swift b/Classes/PKRequestParams.swift new file mode 100644 index 00000000..6c9e3f36 --- /dev/null +++ b/Classes/PKRequestParams.swift @@ -0,0 +1,26 @@ +// +// PKRequestInfo.swift +// Pods +// +// Created by Gal Orlanczyk on 04/04/2017. +// +// + +import Foundation + +/// `PKRequestParamsDecorator` used for getting updated request info +@objc public protocol PKRequestParamsAdapter { + func adapt(requestParams: PKRequestParams) -> PKRequestParams +} + +@objc public class PKRequestParams: NSObject { + + public let url: URL + public let headers: [String: String]? + + init(url: URL, headers: [String: String]?) { + self.url = url + self.headers = headers + } +} + diff --git a/Classes/Player/AssetBuilder.swift b/Classes/Player/AssetBuilder.swift index 928974d9..6cc3171a 100644 --- a/Classes/Player/AssetBuilder.swift +++ b/Classes/Player/AssetBuilder.swift @@ -63,7 +63,7 @@ class AssetBuilder { return nil } - func build(readyCallback: @escaping (Error?, AVAsset?)->Void) -> Void { + func build(readyCallback: @escaping (Error?, AVAsset?) -> Void) -> Void { guard let (source, handlerClass) = getPreferredMediaSource() else { PKLog.error("No playable sources") @@ -80,11 +80,11 @@ class AssetBuilder { protocol AssetHandler { init() - func buildAsset(mediaSource: MediaSource, readyCallback: @escaping (Error?, AVAsset?)->Void) + func buildAsset(mediaSource: MediaSource, readyCallback: @escaping (Error?, AVAsset?) -> Void) } protocol RefreshableAssetHandler: AssetHandler { - func shouldRefreshAsset(mediaSource: MediaSource, refreshCallback: @escaping (Bool)->Void) + func shouldRefreshAsset(mediaSource: MediaSource, refreshCallback: @escaping (Bool) -> Void) func refreshAsset(mediaSource: MediaSource) } diff --git a/Classes/Player/DefaultAssetHandler.swift b/Classes/Player/DefaultAssetHandler.swift index c9f89fbb..768def73 100644 --- a/Classes/Player/DefaultAssetHandler.swift +++ b/Classes/Player/DefaultAssetHandler.swift @@ -18,9 +18,9 @@ class DefaultAssetHandler: AssetHandler { } - func buildAsset(mediaSource: MediaSource, readyCallback: @escaping (Error?, AVAsset?)->Void) { + func buildAsset(mediaSource: MediaSource, readyCallback: @escaping (Error?, AVAsset?) -> Void) { - guard let contentUrl = mediaSource.contentUrl else { + guard let contentUrl = mediaSource.contentUrl, let playbackUrl = mediaSource.playbackUrl else { PKLog.error("Invalid media: no url") readyCallback(AssetError.invalidContentUrl(nil), nil) return @@ -46,7 +46,7 @@ class DefaultAssetHandler: AssetHandler { guard let drmData = mediaSource.drmData?.first else { PKLog.debug("Creating clear AVURLAsset") - readyCallback(nil, AVURLAsset(url: contentUrl)) + readyCallback(nil, AVURLAsset(url: playbackUrl)) return } @@ -63,7 +63,7 @@ class DefaultAssetHandler: AssetHandler { return } - let asset = AVURLAsset(url: contentUrl) + let asset = AVURLAsset(url: playbackUrl) self.assetLoaderDelegate = AssetLoaderDelegate.configureRemotePlay(asset: asset, drmData: fpsData) diff --git a/Classes/Player/MediaEntry.swift b/Classes/Player/MediaEntry.swift index 958fb477..5b05a311 100644 --- a/Classes/Player/MediaEntry.swift +++ b/Classes/Player/MediaEntry.swift @@ -9,10 +9,6 @@ import UIKit import SwiftyJSON -func getJson(_ json: Any) -> JSON { - return json as? JSON ?? JSON(json) -} - @objc public enum MediaType: Int { case live case vod @@ -24,7 +20,7 @@ func getJson(_ json: Any) -> JSON { @objc public var sources: [MediaSource]? @objc public var duration: TimeInterval = 0 @objc public var mediaType: MediaType = .unknown - @objc public var metadata:[String:String]? + @objc public var metadata:[String: String]? private let idKey = "id" private let sourcesKey = "sources" @@ -43,9 +39,9 @@ func getJson(_ json: Any) -> JSON { super.init() } - public init(json: Any?) { + public init(json: Any) { - let jsonObject = getJson(json) + let jsonObject = json as? JSON ?? JSON(json) self.id = jsonObject[idKey].string ?? "" @@ -66,7 +62,13 @@ func getJson(_ json: Any) -> JSON { override public var description: String { get { - return "id : \(self.id), sources: \(self.sources)" + return "id : \(self.id), sources: \(String(describing: self.sources))" + } + } + + func configureMediaSource(withContentRequestAdapter contentRequestAdapter: PKRequestParamsAdapter) { + self.sources?.forEach { mediaSource in + mediaSource.contentRequestAdapter = contentRequestAdapter } } } @@ -81,43 +83,29 @@ func getJson(_ json: Any) -> JSON { case mp3 case unknown - var fileExtension: String { get { switch self { - case .dash: - return "mpd" - case .hls: - return "m3u8" - case .wvm: - return "wvm" - case .mp4: - return "mp4" - case .mp3: - return "mp3" - case .unknown: - return "" + case .dash: return "mpd" + case .hls: return "m3u8" + case .wvm: return "wvm" + case .mp4: return "mp4" + case .mp3: return "mp3" + case .unknown: return "" } } } static func mediaFormat(byfileExtension ext:String) -> MediaFormat{ switch ext { - case "mpd": - return .dash - case "m3u8": - return .hls - case "wvm": - return .wvm - case "mp4": - return .mp4 - case "mp3": - return .mp3 - default: - return .unknown + case "mpd": return .dash + case "m3u8": return .hls + case "wvm": return .wvm + case "mp4": return .mp4 + case "mp3": return .mp3 + default: return .unknown } } - } @objc public var id: String @@ -131,10 +119,20 @@ func getJson(_ json: Any) -> JSON { @objc public var drmData: [DRMParams]? @objc public var mediaFormat: MediaFormat = .unknown @objc public var fileExt: String { - return contentUrl?.pathExtension ?? "" } + /// request params adapter, used to adapt the url. + var contentRequestAdapter: PKRequestParamsAdapter? + /// the playback url, if adapter exists uses it adapt otherwise uses the contentUrl. + var playbackUrl: URL? { + guard let contentUrl = self.contentUrl else { return nil } + if let contentRequestAdapter = self.contentRequestAdapter { + return contentRequestAdapter.adapt(requestParams: PKRequestParams(url: contentUrl, headers: nil)).url + } + return contentUrl + } + private let idKey: String = "id" private let contentUrlKey: String = "url" private let drmDataKey: String = "drmData" @@ -153,7 +151,7 @@ func getJson(_ json: Any) -> JSON { @objc public init(json: Any) { - let sj = getJson(json) + let sj = json as? JSON ?? JSON(json) self.id = sj[idKey].string ?? UUID().uuidString @@ -172,17 +170,13 @@ func getJson(_ json: Any) -> JSON { override public var description: String { get { - return "id : \(self.id), url: \(self.contentUrl)" + return "id : \(self.id), url: \(String(describing: self.contentUrl))" } } } - - - @objc open class DRMParams: NSObject { - public enum Scheme: Int { case widevineCenc case playreadyCenc @@ -203,7 +197,7 @@ func getJson(_ json: Any) -> JSON { @objc public static func fromJSON(_ json: Any) -> DRMParams? { - let sj = getJson(json) + let sj = json as? JSON ?? JSON(json) guard let licenseUri = sj["licenseUri"].string else { return nil } let schemeValue: Int = sj["scheme"].int ?? Scheme.unknown.hashValue diff --git a/Classes/Player/Player.swift b/Classes/Player/Player.swift index 0b7b34db..e3b35e48 100644 --- a/Classes/Player/Player.swift +++ b/Classes/Player/Player.swift @@ -14,23 +14,31 @@ import AVKit func playerShouldPlayAd(_ player: Player) -> Bool } -@objc public protocol Player: NSObjectProtocol { +/// `PlayerSettings` used for optional `Player` settings. +@objc public protocol PlayerSettings { + func set(contentRequestAdapter: PKRequestParamsAdapter) +} + +@objc public protocol Player: PlayerSettings { @objc weak var delegate: PlayerDelegate? { get set } /// The player's associated media entry. @objc weak var mediaEntry: MediaEntry? { get } - /// Get the player's layer component. + /// the player's settings + @objc var settings: PlayerSettings { get } + + /// The player's layer component. @objc var view: UIView! { get } - /// Get/set the current player position. + /// The current player position. @objc var currentTime: TimeInterval { get set } - - /// Get the player's duration. + + /// The player's duration. @objc var isPlaying: Bool { get } - - /// Get the player's duration. + + /// The player's duration. @objc var duration: TimeInterval { get } /// Get the player's current audio track. @@ -39,8 +47,10 @@ import AVKit /// Get the player's current text track. @objc var currentTextTrack: String? { get } - /// Prepare for playing an entry. - /// play when it's ready. + /// The player's session id. the `sessionId` is initialized when the player loads. + @objc var sessionId: UUID { get } + + /// Prepare for playing an entry. play when it's ready. (preparing starts buffering the entry) @objc func prepare(_ config: MediaConfig) /// send play action for the player. @@ -62,7 +72,7 @@ import AVKit @objc func destroy() /// Add Observation to relevant event. - @objc func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (PKEvent)->Void) + @objc func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (PKEvent) -> Void) /// Remove Observation. @objc func removeObserver(_ observer: AnyObject, events: [PKEvent.Type]) diff --git a/Classes/Player/PlayerController.swift b/Classes/Player/PlayerController.swift index 7b134bf0..4d8740ef 100644 --- a/Classes/Player/PlayerController.swift +++ b/Classes/Player/PlayerController.swift @@ -18,6 +18,11 @@ class PlayerController: NSObject, Player { fileprivate var currentPlayer: AVPlayerEngine fileprivate var assetBuilder: AssetBuilder? + fileprivate var contentRequestAdapter: PKRequestParamsAdapter? + + var settings: PlayerSettings { + return self + } public var mediaEntry: MediaEntry? { return self.assetBuilder?.mediaEntry @@ -48,8 +53,11 @@ class PlayerController: NSObject, Player { return self.currentPlayer.view } + public let sessionId = 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)") @@ -63,6 +71,10 @@ 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) + } self.currentPlayer.startPosition = config.startTime self.assetBuilder = AssetBuilder(mediaEntry: config.mediaEntry) self.assetBuilder?.build { (error: Error?, asset: AVAsset?) in @@ -122,6 +134,17 @@ class PlayerController: NSObject, Player { } } +/************************************************************/ +// MARK: - PlayerSettings +/************************************************************/ + +extension PlayerController: PlayerSettings { + + func set(contentRequestAdapter: PKRequestParamsAdapter?) { + self.contentRequestAdapter = contentRequestAdapter + } +} + /************************************************************/ // MARK: - Reachability & Application States Handling /************************************************************/ diff --git a/Classes/Player/PlayerDecoratorBase.swift b/Classes/Player/PlayerDecoratorBase.swift index 6dc625b2..227a2f96 100644 --- a/Classes/Player/PlayerDecoratorBase.swift +++ b/Classes/Player/PlayerDecoratorBase.swift @@ -11,8 +11,8 @@ import AVFoundation import AVKit @objc public class PlayerDecoratorBase: NSObject, Player { - - private var player: Player! + + fileprivate var player: Player! public var delegate: PlayerDelegate? { get { @@ -27,6 +27,10 @@ import AVKit return self.player.mediaEntry } + public var settings: PlayerSettings { + return self.player.settings + } + public var currentTime: TimeInterval { get { return self.player.currentTime @@ -56,6 +60,10 @@ import AVKit return self.player.view } + public var sessionId: UUID { + return self.player.sessionId + } + public func prepare(_ config: MediaConfig) { return self.player.prepare(config) } @@ -113,3 +121,15 @@ import AVKit self.player.selectTrack(trackId: trackId) } } + +/************************************************************/ +// MARK: - PlayerSettings +/************************************************************/ + +extension PlayerDecoratorBase: PlayerSettings { + + public func set(contentRequestAdapter: PKRequestParamsAdapter) { + self.player.set(contentRequestAdapter: contentRequestAdapter) + } +} + From e68f86640ecebe75b724c46f31b17cbbe508fa34 Mon Sep 17 00:00:00 2001 From: Gal Orlanczyk Date: Thu, 6 Apr 2017 16:23:06 +0300 Subject: [PATCH 22/27] FEM-1318 (#139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #FEM-1320 * Added `PlayerSettings` to provide the ability to change our content request adapter. * Added `KalturaPlaybackRequestAdapter` providing adaptation of the content request if we have ‘playManifest’ inside, to add uuid and clientTag params to the request. * * Fixed issue where content request adapter couldn’t be removed. * #FEM-1318 * adapted Kaltura analytics (stats and live) to new sessionId from player (FEM-1320) * if `entryId` will be provided in the Kaltura stats/live stats config use it instead of `mediaEntry.id`. --- .../KalturaLiveStatsPlugin.swift | 20 +++++++------ Plugins/KalturaStats/KalturaStatsPlugin.swift | 29 +++++++++---------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift b/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift index 70294e27..920ee57b 100644 --- a/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift +++ b/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift @@ -182,21 +182,18 @@ public class KalturaLiveStatsPlugin: BaseAnalyticsPlugin { } private func sendLiveEvent(withBufferTime bufferTime: Int32) { + guard let player = self.player, let mediaEntry = player.mediaEntry else { return } + PKLog.debug("sendLiveEvent - Buffer Time: \(bufferTime)") // post event to message bus let event = KalturaLiveStatsEvent.Report(bufferTime: bufferTime) self.messageBus?.post(event) - guard let mediaEntry = self.player?.mediaEntry else { return } - - var sessionId = "" + let entryId: String + let sessionId = player.sessionId.uuidString var baseUrl = "https://stats.kaltura.com/api_v3/index.php" var parterId = "" - if let sId = self.config?.params["sessionId"] as? String { - sessionId = sId - } - if let url = self.config?.params["baseUrl"] as? String { baseUrl = url } @@ -205,6 +202,12 @@ public class KalturaLiveStatsPlugin: BaseAnalyticsPlugin { parterId = String(pId) } + if let eId = self.config?.params["entryId"] as? String { + entryId = eId + } else { + entryId = mediaEntry.id + } + if let builder: RequestBuilder = LiveStatsService.sendLiveStatsEvent(baseURL: baseUrl, partnerId: parterId, eventType: self.isLive ? 1 : 0, @@ -213,7 +216,7 @@ public class KalturaLiveStatsPlugin: BaseAnalyticsPlugin { bitrate: self.lastReportedBitrate, sessionId: sessionId, startTime: self.lastReportedStartTime, - entryId: mediaEntry.id, + entryId: entryId, isLive: isLive, clientVer: PlayKitManager.clientTag, deliveryType: "hls") { @@ -226,5 +229,4 @@ public class KalturaLiveStatsPlugin: BaseAnalyticsPlugin { USRExecutor.shared.send(request: builder.build()) } } - } diff --git a/Plugins/KalturaStats/KalturaStatsPlugin.swift b/Plugins/KalturaStats/KalturaStatsPlugin.swift index b47e1f04..3b79d051 100644 --- a/Plugins/KalturaStats/KalturaStatsPlugin.swift +++ b/Plugins/KalturaStats/KalturaStatsPlugin.swift @@ -100,12 +100,9 @@ public class KalturaStatsPlugin: BaseAnalyticsPlugin { override var playerEventsToRegister: [PlayerEvent.Type] { return [ - PlayerEvent.ended, PlayerEvent.error, - PlayerEvent.pause, PlayerEvent.canPlay, PlayerEvent.playing, - PlayerEvent.seeking, PlayerEvent.seeked, PlayerEvent.stateChanged ] @@ -124,10 +121,6 @@ public class KalturaStatsPlugin: BaseAnalyticsPlugin { PKLog.debug("canPlay event: \(event)") strongSelf.sendMediaLoaded() }) - case let e where e.self == PlayerEvent.ended || e.self == PlayerEvent.pause || e.self == PlayerEvent.seeking: - self.messageBus?.addObserver(self, events: [e.self], block: { [weak self] (event) in - PKLog.debug("\(e.self) event: \(event)") - }) case let e where e.self == PlayerEvent.seeked: self.messageBus?.addObserver(self, events: [e.self], block: { [weak self] (event) in guard let strongSelf = self, let player = self?.player else { return } @@ -277,17 +270,20 @@ public class KalturaStatsPlugin: BaseAnalyticsPlugin { private func sendAnalyticsEvent(action: KalturaStatsEventType) { guard let player = self.player else { return } + + guard let mediaEntry = player.mediaEntry else { + PKLog.error("send analytics failed due to nil mediaEntry") + return + } + PKLog.debug("Action: \(action)") - var sessionId = "" + let entryId: String + let sessionId = player.sessionId.uuidString var baseUrl = "https://stats.kaltura.com/api_v3/index.php" var confId = 0 var parterId = "" - if let sId = self.config?.params["sessionId"] as? String { - sessionId = sId - } - if let cId = self.config?.params["uiconfId"] as? Int { confId = cId } @@ -300,9 +296,10 @@ public class KalturaStatsPlugin: BaseAnalyticsPlugin { parterId = String(pId) } - guard let mediaEntry = player.mediaEntry else { - PKLog.error("send analytics failed due to nil mediaEntry") - return + if let eId = self.config?.params["entryId"] as? String { + entryId = eId + } else { + entryId = mediaEntry.id } guard let builder: KalturaRequestBuilder = OVPStatsService.get(baseURL: baseUrl, @@ -313,7 +310,7 @@ public class KalturaStatsPlugin: BaseAnalyticsPlugin { sessionId: sessionId, position: player.currentTime.toInt32(), uiConfId: confId, - entryId: mediaEntry.id, + entryId: entryId, widgetId: "_\(parterId)", isSeek: hasSeeked) else { return } From 8ef62fa0c04cb555fe4a4a6bd1c9ec7102cfa3f5 Mon Sep 17 00:00:00 2001 From: ElizaSapir Date: Thu, 6 Apr 2017 22:03:50 +0300 Subject: [PATCH 23/27] Fem 1333 (#140) * FEM-1333 #comment support change media on Youbora * FEM-1333 #comment support change media on BaseOTTAnalyticsPlugin * FEM-1333 #comment support changemedia on KalturaStatsPlugin * FEM-1333 #comment support changeMedia on KalturaLiveStatsPlugin --- Classes/PlayerEvent.swift | 2 +- .../AnalyticsCommon/BaseAnalyticsPlugin.swift | 21 +++++++++++++++++-- .../KalturaLiveStatsPlugin.swift | 14 +++++++++++++ Plugins/KalturaStats/KalturaStatsPlugin.swift | 18 ++++++++++++++++ Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift | 20 +++++++++--------- Plugins/Youbora/YouboraEvent.swift | 12 ++++++++++- Plugins/Youbora/YouboraPlugin.swift | 6 +++++- 7 files changed, 78 insertions(+), 15 deletions(-) diff --git a/Classes/PlayerEvent.swift b/Classes/PlayerEvent.swift index 5fabc077..e7ae6c5b 100644 --- a/Classes/PlayerEvent.swift +++ b/Classes/PlayerEvent.swift @@ -8,7 +8,7 @@ import Foundation import AVFoundation -/// An PlayerEvent is a class used to reflect player events. +/// PlayerEvent is a class used to reflect player events. @objc public class PlayerEvent: PKEvent { // All events EXCLUDING error. Assuming error events are treated differently. diff --git a/Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift b/Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift index baf696b1..26a3e805 100644 --- a/Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift +++ b/Plugins/AnalyticsCommon/BaseAnalyticsPlugin.swift @@ -72,11 +72,28 @@ extension PKErrorCode { self.registerEvents() } + public override func onUpdateMedia(mediaConfig: MediaConfig) { + super.onUpdateMedia(mediaConfig: mediaConfig) + self.isFirstPlay = true + } + + public override func onUpdateConfig(pluginConfig: Any) { + super.onUpdateConfig(pluginConfig: pluginConfig) + + guard let config = pluginConfig as? AnalyticsConfig else { + PKLog.error("plugin configis wrong") + return + } + + PKLog.debug("new config::\(String(describing: config))") + self.config = config + } + public override func destroy() { - self.messageBus?.removeObserver(self, events: playerEventsToRegister) + self.messageBus?.removeObserver(self, events: playerEventsToRegister) super.destroy() } - + /************************************************************/ // MARK: - AnalyticsPluginProtocol /************************************************************/ diff --git a/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift b/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift index 920ee57b..8ce6edb7 100644 --- a/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift +++ b/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift @@ -55,6 +55,20 @@ public class KalturaLiveStatsPlugin: BaseAnalyticsPlugin { // MARK: - PKPlugin /************************************************************/ + public override func onUpdateMedia(mediaConfig: MediaConfig) { + self.isLive = false + self.eventIdx = 0 + self.currentBitrate = -1 + self.bufferTime = 0 + self.bufferStartTime = 0 + self.lastReportedBitrate = -1 + self.lastReportedStartTime = 0 + + self.isBuffering = false + + self.timer?.invalidate() + } + public override func destroy() { super.destroy() eventIdx = 0 diff --git a/Plugins/KalturaStats/KalturaStatsPlugin.swift b/Plugins/KalturaStats/KalturaStatsPlugin.swift index 3b79d051..350d747b 100644 --- a/Plugins/KalturaStats/KalturaStatsPlugin.swift +++ b/Plugins/KalturaStats/KalturaStatsPlugin.swift @@ -186,6 +186,24 @@ public class KalturaStatsPlugin: BaseAnalyticsPlugin { // MARK: - PKPlugin /************************************************************/ + public override func onUpdateMedia(mediaConfig: MediaConfig) { + super.onUpdateMedia(mediaConfig: mediaConfig) + self.isWidgetLoaded = false + self.isMediaLoaded = false + self.isBuffering = false + + self.seekPercent = 0.0 + + self.playReached25 = false + self.playReached50 = false + self.playReached75 = false + self.playReached100 = false + self.intervalOn = false + self.hasSeeked = false + + self.timer?.invalidate() + } + public override func destroy() { super.destroy() if let t = self.timer { diff --git a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift index 8472df0a..e825bc66 100644 --- a/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift +++ b/Plugins/Phoenix/BaseOTTAnalyticsPlugin.swift @@ -18,16 +18,22 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt /************************************************************/ // MARK: - PKPlugin /************************************************************/ + + public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) throws { + try super.init(player: player, pluginConfig: pluginConfig, messageBus: messageBus) + AppStateSubject.shared.add(observer: self) + } public override func onUpdateMedia(mediaConfig: MediaConfig) { super.onUpdateMedia(mediaConfig: mediaConfig) - AppStateSubject.shared.add(observer: self) + self.intervalOn = false + self.timer?.invalidate() } public override func destroy() { super.destroy() self.sendAnalyticsEvent(ofType: .stop) - self.stopTimer() + self.timer?.invalidate() AppStateSubject.shared.remove(observer: self) } @@ -70,7 +76,7 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt self.messageBus?.addObserver(self, events: [e.self]) { [weak self] event in guard let strongSelf = self else { return } PKLog.debug("ended event: \(event)") - strongSelf.stopTimer() + strongSelf.timer?.invalidate() strongSelf.sendAnalyticsEvent(ofType: .finish) } case let e where e.self == PlayerEvent.error: @@ -86,7 +92,7 @@ public class BaseOTTAnalyticsPlugin: BaseAnalyticsPlugin, OTTAnalyticsPluginProt // invalidate timer when receiving pause event only after first play // and set intervalOn to false in order to start timer again on play event. if !strongSelf.isFirstPlay { - strongSelf.stopTimer() + strongSelf.timer?.invalidate() strongSelf.intervalOn = false } strongSelf.sendAnalyticsEvent(ofType: .pause) @@ -175,12 +181,6 @@ extension BaseOTTAnalyticsPlugin { } } - fileprivate func stopTimer() { - if let t = self.timer { - t.invalidate() - } - } - fileprivate func sendProgressEvent() { guard let player = self.player else { return } self.sendAnalyticsEvent(ofType: .hit); diff --git a/Plugins/Youbora/YouboraEvent.swift b/Plugins/Youbora/YouboraEvent.swift index 2f886979..3a242bfe 100644 --- a/Plugins/Youbora/YouboraEvent.swift +++ b/Plugins/Youbora/YouboraEvent.swift @@ -12,12 +12,22 @@ import UIKit class Report: YouboraEvent { convenience init(message: String) { - self.init(["message": message]) + self.init([YouboraEvent.messageKey: message]) } } + /// this event notifies when a youbora event is being sent @objc public static let report: YouboraEvent.Type = Report.self + @objc public static let messageKey = "messageKey" + @available(*, unavailable, renamed: "report") @objc public static let youboraReportSent: YouboraEvent.Type = Report.self } + +extension PKEvent { + /// Report Value, PKEvent Data Accessor + @objc public var youboraMessage: String? { + return self.data?[YouboraEvent.messageKey] as? String + } +} diff --git a/Plugins/Youbora/YouboraPlugin.swift b/Plugins/Youbora/YouboraPlugin.swift index 423df215..03d50c1e 100644 --- a/Plugins/Youbora/YouboraPlugin.swift +++ b/Plugins/Youbora/YouboraPlugin.swift @@ -81,6 +81,11 @@ public class YouboraPlugin: BaseAnalyticsPlugin { } } + public override func onUpdateConfig(pluginConfig: Any) { + super.onUpdateConfig(pluginConfig: pluginConfig) + self.setupYouboraManager() + } + public override func destroy() { super.destroy() self.stopMonitoring() @@ -267,4 +272,3 @@ public class YouboraPlugin: BaseAnalyticsPlugin { self.messageBus?.post(eventLog) } } - From 6436f2adf86639cbdfcbbf743321aa5ed1c16ea5 Mon Sep 17 00:00:00 2001 From: Eliza Sapir Date: Sun, 9 Apr 2017 19:16:44 +0300 Subject: [PATCH 24/27] bump v0.1.29 --- Example/PlayKit/Info.plist | 2 +- PlayKit.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/PlayKit/Info.plist b/Example/PlayKit/Info.plist index 86e78c0a..ce89c7be 100644 --- a/Example/PlayKit/Info.plist +++ b/Example/PlayKit/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.1.28 + 0.1.29 CFBundleSignature ???? CFBundleVersion diff --git a/PlayKit.podspec b/PlayKit.podspec index c9abfd90..5ab9b9e8 100644 --- a/PlayKit.podspec +++ b/PlayKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'PlayKit' -s.version = '0.1.28' +s.version = '0.1.29' s.summary = 'PlayKit: Kaltura Mobile Player SDK - iOS' From 995e467387f09a4b4b983302b3ff4277d1b6dba5 Mon Sep 17 00:00:00 2001 From: srivkas Date: Tue, 18 Apr 2017 13:35:12 +0300 Subject: [PATCH 25/27] replace raw value to asString (#141) --- Classes/Providers/OTT/Services/OTTAssetService.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Classes/Providers/OTT/Services/OTTAssetService.swift b/Classes/Providers/OTT/Services/OTTAssetService.swift index 14852321..476d19c5 100644 --- a/Classes/Providers/OTT/Services/OTTAssetService.swift +++ b/Classes/Providers/OTT/Services/OTTAssetService.swift @@ -17,8 +17,8 @@ class OTTAssetService { request .setBody(key: "id", value: JSON(assetId)) .setBody(key: "ks", value: JSON(ks)) - .setBody(key: "type", value: JSON(type.rawValue)) - .setBody(key: "assetReferenceType", value: JSON(type.rawValue)) + .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 { @@ -32,7 +32,7 @@ class OTTAssetService { request .setBody(key: "assetId", value: JSON(assetId)) .setBody(key: "ks", value: JSON(ks)) - .setBody(key: "assetType", value: JSON(type.rawValue)) + .setBody(key: "assetType", value: JSON(type.asString)) .setBody(key: "contextDataParams", value: JSON(playbackContextOptions.toDictionary())) return request } else { @@ -51,7 +51,7 @@ struct PlaybackContextOptions { func toDictionary() -> [String: Any] { var dict: [String: Any] = [:] - dict["context"] = playbackContextType.rawValue + dict["context"] = playbackContextType.asString dict["mediaProtocols"] = protocls if let fileIds = self.assetFileIds { dict["assetFileIds"] = fileIds.joined(separator: ",") From 785e68df381105dee0e75fef1fe18d700f864c66 Mon Sep 17 00:00:00 2001 From: ElizaSapir Date: Wed, 19 Apr 2017 17:51:03 +0300 Subject: [PATCH 26/27] add supported version under CC sdk dependency (#143) --- PlayKit.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PlayKit.podspec b/PlayKit.podspec index 06384044..7fbeb86a 100644 --- a/PlayKit.podspec +++ b/PlayKit.podspec @@ -38,7 +38,7 @@ s.subspec 'GoogleCastAddon' do |ssp| 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**', 'LIBRARY_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**' } - ssp.dependency 'google-cast-sdk' + ssp.dependency 'google-cast-sdk', '3.3.0' ssp.dependency 'PlayKit/Core' end From eca19959768dafde62080f76b999092df33a6292 Mon Sep 17 00:00:00 2001 From: Eliza Sapir Date: Wed, 19 Apr 2017 17:54:07 +0300 Subject: [PATCH 27/27] bump v0.1.30 --- Example/PlayKit/Info.plist | 2 +- PlayKit.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/PlayKit/Info.plist b/Example/PlayKit/Info.plist index ce89c7be..df95e98c 100644 --- a/Example/PlayKit/Info.plist +++ b/Example/PlayKit/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.1.29 + 0.1.30 CFBundleSignature ???? CFBundleVersion diff --git a/PlayKit.podspec b/PlayKit.podspec index f2b0189d..feade816 100644 --- a/PlayKit.podspec +++ b/PlayKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'PlayKit' -s.version = '0.1.29' +s.version = '0.1.30' s.summary = 'PlayKit: Kaltura Mobile Player SDK - iOS'