diff --git a/.swift-version b/.swift-version new file mode 100755 index 00000000..9f55b2cc --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +3.0 diff --git a/.swift_version b/.swift_version new file mode 100644 index 00000000..9f55b2cc --- /dev/null +++ b/.swift_version @@ -0,0 +1 @@ +3.0 diff --git a/.travis.yml b/.travis.yml index 93c22d29..a7ca40f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ before_install: - gem install cocoapods # Since Travis is not always on latest version script: - cd PlayKitFramework - - pod install + - pod update - xcodebuild -scheme PlayKitFramework -workspace PlayKitFramework.xcworkspace notifications: email: diff --git a/Classes/Managers/AppState/AppStateProvider.swift b/Classes/Managers/AppState/AppStateProvider.swift new file mode 100644 index 00000000..1f882ecf --- /dev/null +++ b/Classes/Managers/AppState/AppStateProvider.swift @@ -0,0 +1,70 @@ +// +// AppStateProvider.swift +// Pods +// +// Created by Gal Orlanczyk on 19/01/2017. +// +// + +import Foundation + +/// The delegate of `AppStateProvider`, allows the delegate to inform on app state notifications. +protocol AppStateProviderDelegate: class { + /// fire this delegate function when received observation event. + /// for every observer with the same observation event process the on observe block. + func appStateEventPosted(name: ObservationName) +} + +/// The interface of `AppStateProvider`, allows us to better divide the logic and mock easier. +protocol AppStateProviderProtocol { + var notificationsManager: NotificationsManager { get } + /// Holds all the observation names we will be observing. + /// If you want to observe more events add them here. + var observationNames: Set { get } + weak var delegate: AppStateProviderDelegate? { get } +} + +extension AppStateProviderProtocol { + /// Add observers for the provided notification names. + func addObservers() { + observationNames.forEach { name in + notificationsManager.addObserver(notificationName: name) { notification in + self.delegate?.appStateEventPosted(name: notification.name) + } + } + } + + /// Remove observers for the provided notification names. + func removeObservers() { + notificationsManager.removeAllObservers() + } + +} + +/************************************************************/ +// MARK: - AppStateProvider +/************************************************************/ + +/// The `AppStateProvider` is a provider for receiving events from the system about app states. +/// Used to seperate the events providing from the app state subject and enabling us to mock better. +final class AppStateProvider: AppStateProviderProtocol { + + init(delegate: AppStateProviderDelegate? = nil) { + self.delegate = delegate + } + + var delegate: AppStateProviderDelegate? + + let notificationsManager = NotificationsManager() + + let observationNames: Set = [ + .UIApplicationWillTerminate, + .UIApplicationDidEnterBackground, + .UIApplicationDidBecomeActive, + .UIApplicationWillResignActive, + .UIApplicationWillEnterForeground + ] + +} + + diff --git a/Classes/Managers/AppState/AppStateSubject.swift b/Classes/Managers/AppState/AppStateSubject.swift new file mode 100644 index 00000000..4a2f7f2f --- /dev/null +++ b/Classes/Managers/AppState/AppStateSubject.swift @@ -0,0 +1,168 @@ +// +// AppStateSubject.swift +// Pods +// +// Created by Gal Orlanczyk on 19/01/2017. +// +// + +import Foundation + +/// The interface of `AppStateSubject`, allows us to better divide the logic and mock easier. +protocol AppStateSubjectProtocol: class, AppStateProviderDelegate { + associatedtype InstanceType + static var sharedInstance: InstanceType { get } + /// Lock object for synchronizing access. + var lock: AnyObject { get } + /// The app state events provider. + var appStateProvider: AppStateProvider { get } + /// The current app state observers. + var observers: [AppStateObservable] { get set } + /// States whether currently observing. + /// - note: when mocking set initial value to false. + var isObserving: Bool { get set } +} + +extension AppStateSubjectProtocol { + /// Starts observing the app state events + func startObservingAppState() { + sync { + // if not already observing and has more than 0 oberserver then start observing + if !isObserving { + PKLog.trace("start observing app state") + appStateProvider.addObservers() + isObserving = true + } + } + } + + /// Stops observing the app state events. + func stopObservingAppState() { + sync { + if isObserving { + PKLog.trace("stop observing app state") + appStateProvider.removeObservers() + isObserving = false + } + } + } + + /// Adds an observer to inform when state events are posted. + func add(observer: AppStateObservable) { + sync { + PKLog.trace("add observer, \(observer)") + // if no observers were available start observing now + if observers.count == 0 && !isObserving { + startObservingAppState() + } + observers.append(observer) + } + } + + /// Removes an observer to stop being inform when state events are posted. + func remove(observer: AppStateObservable) { + sync { + for i in 0.. 0 { + PKLog.trace("remove all observers") + observers.removeAll() + stopObservingAppState() + } + } + } + + /************************************************************/ + // MARK: AppStateProviderDelegate + /************************************************************/ + + func appStateEventPosted(name: ObservationName) { + sync { + PKLog.trace("app state event posted with name: \(name.rawValue)") + for observer in self.observers { + let filteredObservations = observer.observations.filter { $0.name == name } + for observation in filteredObservations { + observation.onObserve() + } + } + } + } + + // MARK: Private + /// synchornized function + private func sync(block: () -> ()) { + objc_sync_enter(lock) + block() + objc_sync_exit(lock) + } + +} + +/************************************************************/ +// MARK: - AppStateSubject +/************************************************************/ + +/// The `AppStateSubject` class provides a way to add/remove application state observers. +/// +/// - note: Subject is a class that is both observing and being observered. +/// In our case listening to events using the provider and posting using the obervations onObserve. +/// +/// **For Unit-Testing:** When mocking this object just conform to the `AppStateSubjectProtocol`. +/// For firing events to observers manually use `appStateEventPosted(name: ObservationName)` with the observation name. +final class AppStateSubject: AppStateSubjectProtocol { + + // singleton object and private init to prevent unwanted creation of more objects. + static let sharedInstance = AppStateSubject() + private init() { + self.appStateProvider = AppStateProvider() + self.appStateProvider.delegate = self + } + + let lock: AnyObject = UUID().uuidString as AnyObject + + var observers = [AppStateObservable]() + var appStateProvider: AppStateProvider + var isObserving = false +} + +/************************************************************/ +// MARK: - Types +/************************************************************/ + +/// Used to specify observation name +typealias ObservationName = Notification.Name // used as typealias in case we will change type in the future. + +/// represents a single observation with observation name as the type, and a block to perform when observing. +struct NotificationObservation: Hashable { + var name: ObservationName + var onObserve: () -> Void + + var hashValue: Int { + return name.rawValue.hash + } +} + +func == (lhs: NotificationObservation, rhs: NotificationObservation) -> Bool { + return lhs.name.rawValue == rhs.name.rawValue +} + +/// 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 { + var observations: Set { get } +} diff --git a/Classes/Managers/LocalAssetsManager.swift b/Classes/Managers/LocalAssetsManager.swift index 6e6de5b4..2745929a 100644 --- a/Classes/Managers/LocalAssetsManager.swift +++ b/Classes/Managers/LocalAssetsManager.swift @@ -9,31 +9,45 @@ import Foundation import AVFoundation -// FairPlay is not available in simulators and is only downloadable in iOS10 and up. -fileprivate let canDownloadFairPlay: Bool = { - if TARGET_OS_SIMULATOR==0, #available(iOS 10, *) { - return true - } else { - return false - } -}() - -// Widevine is optional (and not available in simulators) -fileprivate let canDownloadWidevineClassic: Bool = TARGET_OS_SIMULATOR==0 - && NSClassFromString("PlayKit.WidevineClassicAssetHandler") != nil - - /// Manage local (downloaded) assets. public class LocalAssetsManager: NSObject { let storage: LocalDataStore var delegates = Set() + private override init() { + fatalError("Private initializer, use one of the factory methods") + } + + /** + Create a new LocalAssetsManager for DRM-protected content. + Uses the default data-store. + */ + public static func managerWithDefaultDataStore() -> LocalAssetsManager { + return LocalAssetsManager(storage: DefaultLocalDataStore.defaultDataStore()) + } + + /** + Create a new LocalAssetsManager for DRM-protected content. + + - Parameter storage: data store. + */ + public static func manager(storage: LocalDataStore) -> LocalAssetsManager { + return LocalAssetsManager(storage: storage) + } + + /** + Create a new LocalAssetsManager for non-DRM content. + */ + public static func manager() -> LocalAssetsManager { + return LocalAssetsManager(storage: nil) + } + /** Create a new LocalAssetsManager. - Parameter storage: data store. Used for DRM data, and may only be nil if DRM is not used. */ - public init(storage: LocalDataStore?) { + private init(storage: LocalDataStore?) { self.storage = storage ?? NullStore.instance } @@ -52,7 +66,7 @@ public class LocalAssetsManager: NSObject { PKLog.debug("Preparing asset for download; asset.url:", asset.url) - guard #available(iOS 10, *), canDownloadFairPlay else { + guard #available(iOS 10, *), DRMSupport.fairplayOffline else { PKLog.error("Downloading FairPlay content is not supported on device") return } @@ -67,23 +81,27 @@ public class LocalAssetsManager: NSObject { } + /// Create a MediaSource for a local asset. This allows the player to play a downloaded asset. private func createLocalMediaSource(for assetId: String, localURL: URL) -> MediaSource { return LocalMediaSource(storage: self.storage, id: assetId, localContentUrl: localURL) } - + /// Create a MediaEntry for a local asset. This is a convenience function that wraps the result of + /// `createLocalMediaSource(for:localURL:)` with a MediaEntry. public func createLocalMediaEntry(for assetId: String, localURL: URL) -> MediaEntry { let mediaSource = createLocalMediaSource(for: assetId, localURL: localURL) return MediaEntry.init(assetId, sources: [mediaSource]) } + /// Get the preferred MediaSource for download purposes. This function takes into account + /// the capabilities of the device. public func getPreferredDownloadableMediaSource(for mediaEntry: MediaEntry) -> MediaSource? { guard let sources = mediaEntry.sources else {return nil} // On iOS 10 and up: HLS (clear or FP), MP4, WVM // Below iOS10: HLS (only clear), MP4, WVM - if canDownloadFairPlay { + if DRMSupport.fairplayOffline { if let source = sources.first(where: {$0.fileExt=="m3u8"}) { return source } @@ -97,13 +115,14 @@ public class LocalAssetsManager: NSObject { return source } - if canDownloadWidevineClassic, let source = sources.first(where: {$0.fileExt=="wvm"}) { + if DRMSupport.widevineClassic, let source = sources.first(where: {$0.fileExt=="wvm"}) { return source } return nil } + /// Prepare a MediaEntry for download using AVAssetDownloadTask. public func prepareForDownload(of mediaEntry: MediaEntry) -> (AVURLAsset, MediaSource)? { guard let source = getPreferredDownloadableMediaSource(for: mediaEntry) else { return nil } guard let url = source.contentUrl else { return nil } @@ -112,7 +131,8 @@ public class LocalAssetsManager: NSObject { return (avAsset, source) } - public func registerDownloadedAsset(location: URL, mediaSource: MediaSource) { + /// Notifies the SDK that downloading of an asset has finished. + public func assetDownloadFinished(location: URL, mediaSource: MediaSource) { // FairPlay -- nothing to do // Widevine: TODO @@ -123,9 +143,9 @@ public class LocalAssetsManager: NSObject { PKLog.error("LocalDataStore not set") } - public func load(key: String) throws -> Data? { + public func load(key: String) throws -> Data { PKLog.error("LocalDataStore not set") - return nil + throw NSError.init(domain: "LocalAssetsManager", code: -1, userInfo: nil) } public func save(key: String, value: Data) throws { @@ -136,18 +156,31 @@ public class LocalAssetsManager: NSObject { } } -public protocol LocalDataStore { +@objc public protocol LocalDataStore { func save(key: String, value: Data) throws - func load(key: String) throws -> Data? + func load(key: String) throws -> Data func remove(key: String) throws } -public class DefaultLocalDataStore: LocalDataStore { +/// Implementation of LocalDataStore that saves data to files in the Library directory. +public class DefaultLocalDataStore: NSObject, LocalDataStore { + static let pkLocalDataStore = "pkLocalDataStore" let storageDirectory: URL - public init() throws { - self.storageDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) + public static func defaultDataStore() -> DefaultLocalDataStore? { + return try? DefaultLocalDataStore(directory: .libraryDirectory) + } + + private override init() { + fatalError("Private initializer, use a factory or `init(directory:)`") + } + + public init(directory: FileManager.SearchPathDirectory) throws { + let baseDir = try FileManager.default.url(for: directory, in: .userDomainMask, appropriateFor: nil, create: false) + self.storageDirectory = baseDir.appendingPathComponent(DefaultLocalDataStore.pkLocalDataStore, isDirectory: true) + + try FileManager.default.createDirectory(at: self.storageDirectory, withIntermediateDirectories: true, attributes: nil) } private func file(_ key: String) -> URL { @@ -158,7 +191,7 @@ public class DefaultLocalDataStore: LocalDataStore { try value.write(to: file(key), options: .atomic) } - public func load(key: String) throws -> Data? { + public func load(key: String) throws -> Data { return try Data.init(contentsOf: file(key), options: []) } diff --git a/Classes/Managers/NotificationsManager.swift b/Classes/Managers/NotificationsManager.swift new file mode 100644 index 00000000..844d2bb9 --- /dev/null +++ b/Classes/Managers/NotificationsManager.swift @@ -0,0 +1,72 @@ +// +// NotificationsManager.swift +// Pods +// +// Created by Gal Orlanczyk on 19/01/2017. +// +// + +import Foundation + +/// The `NotificationsManager` objects provides a mechanism for adding/removing observers within a program +final class NotificationsManager { + + let notificationCenter = NotificationCenter.default + + /// lock object for synchronizing access + let lock: AnyObject = UUID().uuidString as AnyObject + + /// Holds all the notification observers tokens + var observerTokens = [NSObjectProtocol]() + + + /// Adds an observer for notification name, appends the token to the token list and returns it. + /// - returns: The added observer token + @discardableResult + func addObserver(notificationName: Notification.Name, + object: Any? = nil, + queue: DispatchQueue = DispatchQueue.main, + using block: @escaping (Notification) -> Void) -> NSObjectProtocol { + + let observerToken = notificationCenter.addObserver(forName: notificationName, + object: object, + queue: OperationQueue.main, + using: block) + sync { + observerTokens.append(observerToken) + } + return observerToken + } + + /// Removes an observer + func remove(observer: AnyObject) { + sync { + notificationCenter.removeObserver(observer) + for i in 0.. ()) { + objc_sync_enter(lock) + block() + objc_sync_exit(lock) + } +} + diff --git a/Classes/Managers/PlayKitManager.swift b/Classes/Managers/PlayKitManager.swift index cd9e1c39..82362ca8 100644 --- a/Classes/Managers/PlayKitManager.swift +++ b/Classes/Managers/PlayKitManager.swift @@ -8,32 +8,60 @@ import UIKit + +/** + Manager class used for: + - creating `Player` objects. + - creating and registering plugins. + */ public class PlayKitManager: NSObject { - public static let versionString: String = Bundle.init(for: PlayKitManager.self) + // private init to prevent initializing this singleton + private override init() { + if type(of: self) != PlayKitManager.self { + fatalError("Private initializer, use shared instance instead") + } + } + + public static let versionString: String = Bundle(for: PlayKitManager.self) .object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String public static let clientTag = "playkit/ios-\(versionString)" - public static let sharedInstance : PlayKitManager = PlayKitManager() + @objc(sharedInstance) public static let shared: PlayKitManager = PlayKitManager() var pluginRegistry = Dictionary() - public func loadPlayer(config: PlayerConfig) -> Player { + + /// Loads and returns a player object using a provided configuration. + /// + /// - Important: In order to start buffering the video after loading the player + /// you must call prepare on the player with the same configuration. + /// ```` + /// player = PlayKitManager.sharedInstance.loadPlayer(config: config) + /// player.prepare(config) + /// ```` + /// + /// - Parameter config: The configuration object to load the player with. + /// - Returns: A player loaded using the provided configuration. + public func loadPlayer(pluginConfig: PluginConfig?) -> Player { let loader = PlayerLoader() - loader.load(config) + loader.load(pluginConfig: pluginConfig) return loader } - public func registerPlugin(_ pluginClass: PKPlugin.Type) { - pluginRegistry[pluginClass.pluginName] = pluginClass + public func registerPlugin(_ pluginClass: Plugin.Type) { + guard let pluginType = pluginClass as? PKPlugin.Type else { + fatalError("plugin class must be of type PKPlugin") + } + pluginRegistry[pluginType.pluginName] = pluginType } - func createPlugin(name: String) -> PKPlugin? { + func createPlugin(name: String, player: Player, pluginConfig: Any?, messageBus: MessageBus) -> PKPlugin? { let pluginClass = pluginRegistry[name] guard pluginClass != nil else { return nil } - return pluginClass?.init() + return pluginClass?.init(player: player, pluginConfig: pluginConfig, messageBus: messageBus) } } diff --git a/Classes/MessageBus.swift b/Classes/MessageBus.swift index fd8f32da..451d28b3 100644 --- a/Classes/MessageBus.swift +++ b/Classes/MessageBus.swift @@ -10,14 +10,14 @@ import Foundation private struct Observation { weak var observer: AnyObject? - let block: (_ info: Any)->Void + let block: (PKEvent)->Void } public class MessageBus: NSObject { - private var observations = [String: [Observation]]() + private var observations = [String : [Observation]]() private let lock: AnyObject = UUID().uuidString as AnyObject - public func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (_ info: Any)->Void) { + public func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (PKEvent)->Void) { sync { events.forEach { (et) in let typeId = NSStringFromClass(et) @@ -26,6 +26,7 @@ public class MessageBus: NSObject { if array == nil { array = [] } + array!.append(Observation(observer: observer, block: block)) observations[typeId] = array } @@ -36,6 +37,7 @@ public class MessageBus: NSObject { sync { events.forEach { (et) in let typeId = NSStringFromClass(et) + if let array = observations[typeId] { observations[typeId] = array.filter { $0.observer! !== observer } } else { @@ -46,12 +48,12 @@ public class MessageBus: NSObject { } public func post(_ event: PKEvent) { - let typeId = NSStringFromClass(type(of:event)) - sync { + DispatchQueue.main.async { [weak self] in + let typeId = NSStringFromClass(type(of:event)) // TODO: remove nil observers - if let array = observations[typeId] { + if let array = self?.observations[typeId] { array.forEach { - if $0.observer != nil { + if $0.self.observer != nil { $0.block(event) } } diff --git a/Classes/Network/Request.swift b/Classes/Network/Request.swift index a0ec8ed7..77c7a6d3 100644 --- a/Classes/Network/Request.swift +++ b/Classes/Network/Request.swift @@ -128,7 +128,7 @@ public class RequestBuilder: NSObject { public func setParam(key: String, value:String) -> Self { if var params = self.urlParams { - self.urlParams![key] = value + params[key] = value }else{ self.urlParams = [key:value] } diff --git a/Classes/Network/Result.swift b/Classes/Network/Result.swift index c0259d8b..512c70b2 100644 --- a/Classes/Network/Result.swift +++ b/Classes/Network/Result.swift @@ -9,7 +9,7 @@ import UIKit -public class Result { +public class Result: NSObject { public var data: T? = nil public var error: Error? = nil diff --git a/Classes/Network/SessionProvider.swift b/Classes/Network/SessionProvider.swift index 448f4589..6533e652 100644 --- a/Classes/Network/SessionProvider.swift +++ b/Classes/Network/SessionProvider.swift @@ -14,9 +14,6 @@ public protocol SessionProvider { var partnerId: Int64 { get } func loadKS(completion: @escaping (_ result :Result) -> Void) - - - } diff --git a/Classes/Player/AVPlayerEngine.swift b/Classes/Player/AVPlayerEngine.swift index a661cfd3..315b74a6 100644 --- a/Classes/Player/AVPlayerEngine.swift +++ b/Classes/Player/AVPlayerEngine.swift @@ -11,7 +11,9 @@ import AVFoundation import AVKit import CoreMedia -class AVPlayerEngine : AVPlayer { +/// An AVPlayerEngine is a controller used to manage the playback and timing of a media asset. +/// It provides the interface to control the player’s behavior such as its ability to play, pause, and seek to various points in the timeline. +class AVPlayerEngine: AVPlayer { // MARK: Player Properties @@ -19,8 +21,8 @@ class AVPlayerEngine : AVPlayer { let assetKeysRequiredToPlay = [ "playable", "tracks", - "hasProtectedContent", - ] + "hasProtectedContent" + ] private var avPlayerLayer: AVPlayerLayer! @@ -29,6 +31,12 @@ class AVPlayerEngine : AVPlayer { private var isObserved: Bool = false private var tracksManager = TracksManager() private var lastBitrate: Double = 0 + private var isDestroyed = false + + /// Indicates whether the current items was played until the end. + /// + /// - note: Used for preventing 'pause' events to be sent after 'ended' event. + private var isPlayedToEndTime: Bool = false // AVPlayerItem.currentTime() and the AVPlayerItem.timebase's rate are not KVO observable. We check their values regularly using this timer. private var nonObservablePropertiesUpdateTimer: Timer? @@ -59,16 +67,18 @@ class AVPlayerEngine : AVPlayer { PKLog.trace("set currentPosition: \(currentPosition)") let newTime = rangeStart + CMTimeMakeWithSeconds(newValue, 1) - super.seek(to: newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) { (isSeeked: Bool) in + super.seek(to: newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) { [unowned self] (isSeeked: Bool) in if isSeeked { - self.postEvent(event: PlayerEvents.seeked()) + // when seeked successfully reset player reached end time indicator + self.isPlayedToEndTime = false + self.post(event: PlayerEvent.Seeked()) PKLog.trace("seeked") } else { PKLog.error("seek faild") } } - self.postEvent(event: PlayerEvents.seeking()) + self.post(event: PlayerEvent.Seeking()) } } @@ -98,7 +108,6 @@ class AVPlayerEngine : AVPlayer { public var isPlaying: Bool { guard let currentItem = self.currentItem else { PKLog.error("current item is empty") - return false } @@ -156,10 +165,14 @@ class AVPlayerEngine : AVPlayer { self.onEventBlock = nil self.nonObservablePropertiesUpdateTimer = nil + + AppStateSubject.sharedInstance.add(observer: self) } deinit { - self.destroy() + if !isDestroyed { + self.destroy() + } } private func startOrResumeNonObservablePropertiesUpdateTimer() { @@ -189,7 +202,7 @@ class AVPlayerEngine : AVPlayer { if self.rate == 0 { PKLog.trace("play player") - self.postEvent(event: PlayerEvents.play()) + self.post(event: PlayerEvent.Play()) super.play() } } @@ -202,6 +215,10 @@ class AVPlayerEngine : AVPlayer { self.avPlayerLayer = nil self._view = nil self.onEventBlock = nil + // removes app state observer + AppStateSubject.sharedInstance.remove(observer: self) + self.replaceCurrentItem(with: nil) + self.isDestroyed = true } @available(iOS 9.0, *) @@ -312,8 +329,9 @@ class AVPlayerEngine : AVPlayer { removeObserver(self, forKeyPath: keyPath, context: &observerContext) } + NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, object: nil) + NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemNewAccessLogEntry, object: nil) - NotificationCenter.default.removeObserver(self) } func onAccessLogEntryNotification(notification: Notification) { @@ -325,7 +343,7 @@ class AVPlayerEngine : AVPlayer { if lastEvent.indicatedBitrate != self.lastBitrate { self.lastBitrate = lastEvent.indicatedBitrate PKLog.trace("currentBitrate:: \(self.lastBitrate)") - self.postEvent(event: PlayerEvents.playbackParamsUpdated(currentBitrate: self.lastBitrate)) + self.post(event: PlayerEvent.PlaybackParamsUpdated(currentBitrate: self.lastBitrate)) } } } @@ -335,15 +353,16 @@ class AVPlayerEngine : AVPlayer { self.postStateChange(newState: newState, oldState: self.currentState) self.currentState = newState - self.postEvent(event: PlayerEvents.error()) + self.post(event: PlayerEvent.Error()) } public func playerPlayedToEnd(notification: NSNotification) { let newState = PlayerState.idle self.postStateChange(newState: newState, oldState: self.currentState) self.currentState = newState + self.isPlayedToEndTime = true - self.postEvent(event: PlayerEvents.ended()) + self.post(event: PlayerEvent.Ended()) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { @@ -369,24 +388,18 @@ class AVPlayerEngine : AVPlayer { case #keyPath(currentItem.playbackBufferEmpty): self.handleBufferEmptyChange() case #keyPath(currentItem.duration): - event = PlayerEvents.durationChange(duration: CMTimeGetSeconds((self.currentItem?.duration)!)) + event = PlayerEvent.DurationChanged(duration: CMTimeGetSeconds((self.currentItem?.duration)!)) case #keyPath(rate): - if rate > 0 { - self.startOrResumeNonObservablePropertiesUpdateTimer() - } else { - self.nonObservablePropertiesUpdateTimer?.invalidate() - event = PlayerEvents.pause() - } + event = handleRate() case #keyPath(currentItem.status): event = self.handleStatusChange() case #keyPath(currentItem): self.handleItemChange() - default: super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } - self.postEvent(event: event) + self.post(event: event) } private func handleLikelyToKeepUp() { @@ -405,12 +418,31 @@ class AVPlayerEngine : AVPlayer { } } + + /// Handles change in player rate + /// + /// - Returns: The event to post, rate <= 0 means pause event. + private func handleRate() -> PKEvent? { + var event: PKEvent? = nil + + 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 { + event = PlayerEvent.Pause() + } + } + return event + } + private func handleStatusChange() -> PKEvent? { var event: PKEvent? = nil if currentItem?.status == .readyToPlay { let newState = PlayerState.ready - self.postEvent(event: PlayerEvents.loadedMetadata()) + self.post(event: PlayerEvent.LoadedMetadata()) if self.startPosition > 0 { self.currentPosition = self.startPosition @@ -418,19 +450,19 @@ class AVPlayerEngine : AVPlayer { } self.tracksManager.handleTracks(item: self.currentItem, block: { (tracks: PKTracks) in - self.postEvent(event: PlayerEvents.tracksAvailable(tracks: tracks)) + self.post(event: PlayerEvent.TracksAvailable(tracks: tracks)) }) self.postStateChange(newState: newState, oldState: self.currentState) self.currentState = newState - event = PlayerEvents.canPlay() + event = PlayerEvent.CanPlay() } else if currentItem?.status == .failed { let newState = PlayerState.error self.postStateChange(newState: newState, oldState: self.currentState) self.currentState = newState - event = PlayerEvents.error() + event = PlayerEvent.Error() } return event @@ -440,9 +472,11 @@ class AVPlayerEngine : AVPlayer { let newState = PlayerState.idle self.postStateChange(newState: newState, oldState: self.currentState) self.currentState = newState + // in case item changed reset player reached end time indicator + isPlayedToEndTime = false } - private func postEvent(event: PKEvent?) { + fileprivate func post(event: PKEvent?) { if let currentEvent: PKEvent = event { PKLog.trace("onEvent:: \(currentEvent)") @@ -464,9 +498,9 @@ class AVPlayerEngine : AVPlayer { private func postStateChange(newState: PlayerState, oldState: PlayerState) { PKLog.trace("stateChanged:: new:\(newState) old:\(oldState)") - let stateChangedEvent: PKEvent = PlayerEvents.stateChanged(newState: newState, oldState: oldState) + let stateChangedEvent: PKEvent = PlayerEvent.StateChanged(newState: newState, oldState: oldState) - self.postEvent(event: stateChangedEvent) + self.post(event: stateChangedEvent) } // MARK: - Non Observable Properties @@ -477,12 +511,29 @@ class AVPlayerEngine : AVPlayer { if timebaseRate > 0 { self.nonObservablePropertiesUpdateTimer?.invalidate() - self.postEvent(event: PlayerEvents.playing()) + self.post(event: PlayerEvent.Playing()) } - PKLog.trace("timebaseRate:: \(timebaseRate)") } } } } } + +/************************************************************/ +// MARK: - App State Handling +/************************************************************/ + +extension AVPlayerEngine: AppStateObservable { + + var observations: Set { + return [ + NotificationObservation(name: .UIApplicationWillTerminate) { [unowned self] in + PKLog.trace("player: \(self)\n will terminate, destroying...") + self.destroy() + } + ] + } +} + + diff --git a/Classes/Player/AssetBuilder.swift b/Classes/Player/AssetBuilder.swift index 36c1578b..5ae5e6fd 100644 --- a/Classes/Player/AssetBuilder.swift +++ b/Classes/Player/AssetBuilder.swift @@ -18,45 +18,60 @@ class AssetBuilder { self.mediaEntry = mediaEntry } - func build(readyCallback: @escaping (Error?, AVAsset?)->Void) -> Void { + public func getPreferredMediaSource() -> (MediaSource, AssetHandler.Type)? { - // Select source and handler - guard let sources = mediaEntry.sources else { return } + guard let sources = mediaEntry.sources else {return nil} - var selection: (source: MediaSource, handler: AssetHandler.Type)? + let defaultHandler = DefaultAssetHandler.self - // Iterate over all handlers - var handlers: [AssetHandler.Type] = [DefaultAssetHandler.self] + // Preference: Local, HLS, FPS*, MP4, WVM* - if let type = NSClassFromString("PlayKit.WidevineClassicAssetHandler") { - handlers.append(type as! AssetHandler.Type) + if let source = sources.first(where: {$0 is LocalMediaSource}) { + if source.fileExt == "wvm" { + return (source, DRMSupport.widevineClassicHandler!) + } else { + return (source, defaultHandler) + } } - for handler in handlers { - // Select the first source that the handler can play. - if let playableSource = sources.first(where: handler.sourceFilter) { - selection = (source: playableSource, handler: handler) - break // don't ask the other handlers + if DRMSupport.fairplay { + if let source = sources.first(where: {$0.fileExt=="m3u8"}) { + return (source, defaultHandler) + } + } else { + if let source = sources.first(where: {$0.fileExt=="m3u8" && ($0.drmData == nil || $0.drmData!.isEmpty) }) { + return (source, defaultHandler) } } - // Check if something was selected - guard let selected = selection else { + if let source = sources.first(where: {$0.fileExt=="mp4"}) { + return (source, defaultHandler) + } + + if DRMSupport.widevineClassic, let source = sources.first(where: {$0.fileExt=="wvm"}) { + return (source, DRMSupport.widevineClassicHandler!) + } + + return nil + } + + func build(readyCallback: @escaping (Error?, AVAsset?)->Void) -> Void { + + guard let (source, handlerClass) = getPreferredMediaSource() else { PKLog.error("No playable sources") readyCallback(AssetError.noPlayableSources, nil) return } - + // Build the asset - let handler = selected.handler.init() - handler.buildAsset(mediaSource: selected.source, readyCallback: readyCallback) + let handler = handlerClass.init() + handler.buildAsset(mediaSource: source, readyCallback: readyCallback) self.assetHandler = handler } } protocol AssetHandler { init() - static var sourceFilter: (MediaSource)->Bool {get} func buildAsset(mediaSource: MediaSource, readyCallback: @escaping (Error?, AVAsset?)->Void) } @@ -67,3 +82,37 @@ enum AssetError : Error { case invalidContentUrl(URL?) case noPlayableSources } + +class DRMSupport { + // FairPlay is not available in simulators and before iOS8 + static let fairplay: Bool = { + if TARGET_OS_SIMULATOR==0, #available(iOS 8, *) { + return true + } else { + return false + } + }() + + // FairPlay is not available in simulators and is only downloadable in iOS10 and up. + static let fairplayOffline: Bool = { + if TARGET_OS_SIMULATOR==0, #available(iOS 10, *) { + return true + } else { + return false + } + }() + + // Widevine is optional (and not available in simulators) + static let widevineClassic = widevineClassicHandler != nil + + // Preload the Widevine Classic Handler, if available + static let widevineClassicHandler: AssetHandler.Type? = { + if TARGET_OS_SIMULATOR != 0 { + return nil + } + return NSClassFromString("PlayKit.WidevineClassicAssetHandler") as? AssetHandler.Type + }() +} + + + diff --git a/Classes/Player/AssetLoaderDelegate.swift b/Classes/Player/AssetLoaderDelegate.swift index a3196e76..968039dd 100644 --- a/Classes/Player/AssetLoaderDelegate.swift +++ b/Classes/Player/AssetLoaderDelegate.swift @@ -27,7 +27,6 @@ class AssetLoaderDelegate: NSObject { /// The DispatchQueue to use for AVAssetResourceLoaderDelegate callbacks. fileprivate static let resourceLoadingRequestQueue = DispatchQueue(label: "com.kaltura.playkit.resourcerequests") - private let storage: LocalDataStore? private let drmData: FairPlayDRMData? @@ -123,7 +122,7 @@ class AssetLoaderDelegate: NSObject { PKLog.debug("Got response in \(endTime-startTime) sec") let ckc = try self.parseServerResponse(data: data, error: error) callback(Result(data: ckc)) - } catch let e as Error { + } catch let e { callback(Result(error: e)) } }) diff --git a/Classes/Player/DefaultAssetHandler.swift b/Classes/Player/DefaultAssetHandler.swift index af0b5dbf..a4ddf8a5 100644 --- a/Classes/Player/DefaultAssetHandler.swift +++ b/Classes/Player/DefaultAssetHandler.swift @@ -18,36 +18,6 @@ class DefaultAssetHandler: AssetHandler { } - static let sourceFilter = { (_ src: MediaSource) -> Bool in - - // FIXME: extension is not the best criteria here, use format when that's available. - let ext = src.fileExt - - // mp4 is always supported - if ext == "mp4" { - return true - } - - // 'movpkg' was downloaded here. - if ext == "movpkg" { - return true - } - - // The only other option is HLS - guard ext == "m3u8" else { - return false - } - - // DRM is not supported on simulators - if let drmData = src.drmData, drmData.count > 0 && TARGET_OS_SIMULATOR != 0 { - return false - } - - // Source is HLS, with or without DRM. - return true - } - - func buildAsset(mediaSource: MediaSource, readyCallback: @escaping (Error?, AVAsset?)->Void) { guard let contentUrl = mediaSource.contentUrl else { @@ -87,14 +57,12 @@ class DefaultAssetHandler: AssetHandler { return } - guard let fpsCertificate = fpsData.fpsCertificate else { + guard fpsData.fpsCertificate != nil else { PKLog.error("Missing FPS Certificate") readyCallback(AssetError.noFpsCertificate, nil) return } - let assetName = mediaSource.id - let asset = AVURLAsset(url: contentUrl) self.assetLoaderDelegate = AssetLoaderDelegate.configureRemotePlay(asset: asset, drmData: fpsData) diff --git a/Classes/Player/MediaEntry.swift b/Classes/Player/MediaEntry.swift index 61a43a77..b6cf082c 100644 --- a/Classes/Player/MediaEntry.swift +++ b/Classes/Player/MediaEntry.swift @@ -22,6 +22,7 @@ public class MediaEntry: NSObject { public var sources: [MediaSource]? public var duration: Int64? public var mediaType: MediaType? + public var metadata:[String:String]? private let idKey = "id" private let sourcesKey = "sources" @@ -63,7 +64,7 @@ public class MediaEntry: NSObject { } override public var description: String { - get{ + get { return "id : \(self.id), sources: \(self.sources)" } } @@ -119,11 +120,12 @@ public class MediaSource: NSObject { self.init(id, contentUrl: nil) } - public init(_ id: String, contentUrl: URL?, mimeType: String? = nil, drmData: [DRMData]? = nil) { + public init(_ id: String, contentUrl: URL?, mimeType: String? = nil, drmData: [DRMData]? = nil, sourceType: SourceType? = nil) { self.id = id self.contentUrl = contentUrl self.mimeType = mimeType self.drmData = drmData + self.sourceType = sourceType } public init(json: Any) { @@ -132,7 +134,7 @@ public class MediaSource: NSObject { self.id = sj[idKey].string ?? UUID().uuidString - self.contentUrl = sj[contentUrlKey].URL + self.contentUrl = sj[contentUrlKey].url self.mimeType = sj[mimeTypeKey].string @@ -148,7 +150,7 @@ public class MediaSource: NSObject { } override public var description: String { - get{ + get { return "id : \(self.id), url: \(self.contentUrl)" } } diff --git a/Classes/Player/PKEvent.swift b/Classes/Player/PKEvent.swift index e60a6511..b5b17e68 100644 --- a/Classes/Player/PKEvent.swift +++ b/Classes/Player/PKEvent.swift @@ -8,5 +8,89 @@ import Foundation +/// PKEvent public class PKEvent: NSObject { + // Events that have payload must provide it as a dictionary for objective-c compat. + public let data: [String : AnyObject]? + + public required init(_ data: [String : AnyObject]? = nil) { + self.data = data + } +} + +// MARK: - PKEvent Data Accessors Extension +extension PKEvent { + // MARK: - Event Data Keys + struct EventDataKeys { + static let Duration = "duration" + static let Tracks = "tracks" + static let CurrentBitrate = "currentBitrate" + static let OldState = "oldState" + static let NewState = "newState" + static let Error = "error" + } + + // MARK: Player Data Accessors + + /// Duration Value, PKEvent Data Accessor + public var duration: NSNumber? { + return self.data?[EventDataKeys.Duration] as? NSNumber + } + + /// Tracks Value, PKEvent Data Accessor + public var tracks: PKTracks? { + return self.data?[EventDataKeys.Tracks] as? PKTracks + } + + /// Current Bitrate Value, PKEvent Data Accessor + public var currentBitrate: NSNumber? { + return self.data?[EventDataKeys.CurrentBitrate] as? NSNumber + } + + /// Current Old State Value, PKEvent Data Accessor + public var oldState: PlayerState { + guard let oldState = self.data?[EventDataKeys.OldState] as? PlayerState else { + return PlayerState.unknown + } + + return oldState + } + + /// Current New State Value, PKEvent Data Accessor + public var newState: PlayerState { + guard let newState = self.data?[EventDataKeys.NewState] as? PlayerState else { + return PlayerState.unknown + } + + return newState + } + + /// Associated error from error event, PKEvent Data Accessor + public var error: NSError? { + return self.data?[EventDataKeys.Error] as? NSError + } + + // MARK: - Ad Data Keys + struct AdEventDataKeys { + static let MediaTime = "mediaTime" + static let TotalTime = "totalTime" + static let WebOpener = "webOpener" + } + + // MARK: Ad Data Accessors + + /// MediaTime, PKEvent Ad Data Accessor + public var mediaTime: NSNumber? { + return self.data?[AdEventDataKeys.MediaTime] as? NSNumber + } + + /// TotalTime, PKEvent Ad Data Accessor + public var totalTime: NSNumber? { + return self.data?[AdEventDataKeys.TotalTime] as? NSNumber + } + + /// WebOpener, PKEvent Ad Data Accessor + public var webOpener: NSObject? { + return self.data?[AdEventDataKeys.WebOpener] as? NSObject + } } diff --git a/Classes/Player/Player.swift b/Classes/Player/Player.swift index d7d5972a..448eae1c 100644 --- a/Classes/Player/Player.swift +++ b/Classes/Player/Player.swift @@ -10,7 +10,7 @@ import UIKit import AVFoundation import AVKit -@objc public protocol PlayerDelegate: class { +@objc public protocol PlayerDelegate { func playerShouldPlayAd(_ player: Player) -> Bool func player(_ player: Player, failedWith error: String) } @@ -37,7 +37,7 @@ import AVKit /** Get the player's duration. */ - var duration: Double { get } + var duration: TimeInterval { get } var currentAudioTrack: String? { get } @@ -47,7 +47,7 @@ import AVKit Prepare for playing an entry. play when it's ready. */ - func prepare(_ config: PlayerConfig) + func prepare(_ config: MediaConfig) /** Convenience method for setting shouldPlayWhenReady to true. @@ -66,7 +66,7 @@ import AVKit /** Prepare for playing the next entry. */ - func prepareNext(_ config: PlayerConfig) -> Bool + func prepareNext(_ config: MediaConfig) -> Bool /** Load the entry that was prepared with prepareNext(), without waiting for the current entry to end. @@ -78,7 +78,7 @@ import AVKit */ func destroy() - func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (_ info: Any)->Void) + func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (PKEvent)->Void) func removeObserver(_ observer: AnyObject, events: [PKEvent.Type]) diff --git a/Classes/Player/PlayerConfig.swift b/Classes/Player/PlayerConfig.swift index 6049c354..7f7bd7ff 100644 --- a/Classes/Player/PlayerConfig.swift +++ b/Classes/Player/PlayerConfig.swift @@ -8,50 +8,53 @@ import Foundation -public class PlayerConfig: NSObject { - public var mediaEntry : MediaEntry? - public var startTime : TimeInterval = 0 - public var allowPlayerEngineExpose = false - public var subtitleLanguage: String? - public var audioLanguage: String? - public var plugins: [String : AnyObject?]? - - // Builders - @discardableResult - public func set(mediaEntry: MediaEntry) -> Self { +/// A `MediaConfig` object defines behavior and info to use when preparing a `Player` object. +public class MediaConfig: NSObject { + public var mediaEntry: MediaEntry + public var startTime: TimeInterval = 0 + + override public var description: String { + return "Media config, mediaEntry: \(self.mediaEntry)\nstartTime: \(self.startTime)" + } + + public init(mediaEntry: MediaEntry, startTime: TimeInterval = 0) { self.mediaEntry = mediaEntry - return self + self.startTime = startTime } - - @discardableResult - public func set(allowPlayerEngineExpose: Bool) -> Self { - self.allowPlayerEngineExpose = allowPlayerEngineExpose - return self + + public static func config(mediaEntry: MediaEntry) -> MediaConfig { + return MediaConfig.init(mediaEntry: mediaEntry) } - @discardableResult - public func set(startTime: TimeInterval) -> Self { - self.startTime = startTime - return self + public static func config(mediaEntry: MediaEntry, startTime: TimeInterval) -> MediaConfig { + return MediaConfig.init(mediaEntry: mediaEntry, startTime: startTime) } - @discardableResult - public func set(subtitleLanguage: String) -> Self { - self.subtitleLanguage = subtitleLanguage - return self - } + /// Private init. + private override init() { + fatalError("Private initializer, use `init(mediaEntry:startTime:)`") + } +} + +/// A `PluginConfig` object defines config to use when loading a plugin object. +public class PluginConfig: NSObject { + /// Plugins config dictionary holds [plugin name : plugin config] + @objc public var config: [String : Any] + + override public var description: String { + return "Plugin config:\n\(self.config)" + } - @discardableResult - public func set(audioLanguage: String) -> Self { - self.audioLanguage = audioLanguage - return self + public init(config: [String : Any]) { + self.config = config } - @discardableResult - public func set(plugins: [String : AnyObject?]) -> Self { - self.plugins = plugins - return self + /// Private init. + private override init() { + fatalError("Private initializer, use `init(config:)`") } } + + diff --git a/Classes/Player/PlayerController.swift b/Classes/Player/PlayerController.swift index 4a98b0ac..0a642214 100644 --- a/Classes/Player/PlayerController.swift +++ b/Classes/Player/PlayerController.swift @@ -12,6 +12,13 @@ import AVKit class PlayerController: NSObject, Player { + var onEventBlock: ((PKEvent)->Void)? + + var delegate: PlayerDelegate? + + private var currentPlayer: AVPlayerEngine? + private var assetBuilder: AssetBuilder? + public var duration: Double { get { guard let currentPlayer = self.currentPlayer else { @@ -34,12 +41,6 @@ class PlayerController: NSObject, Player { } } - var onEventBlock: ((PKEvent)->Void)? - - var delegate: PlayerDelegate? - - private var currentPlayer: AVPlayerEngine? - private var assetBuilder: AssetBuilder? public var currentTime: TimeInterval { get { @@ -76,21 +77,17 @@ class PlayerController: NSObject, Player { } } - public init(mediaEntry: PlayerConfig) { + public override init() { super.init() self.currentPlayer = AVPlayerEngine() - self.currentPlayer?.onEventBlock = { [unowned self] (event:PKEvent) in + self.currentPlayer?.onEventBlock = { [weak self] event in PKLog.trace("postEvent:: \(event)") - - if let block = self.onEventBlock { - block(event) - } + self?.onEventBlock?(event) } - self.onEventBlock = nil } - func prepare(_ config: PlayerConfig) { + func prepare(_ config: MediaConfig) { if let player = self.currentPlayer { player.startPosition = config.startTime @@ -129,7 +126,7 @@ class PlayerController: NSObject, Player { self.currentPlayer?.currentPosition = CMTimeGetSeconds(time) } - func prepareNext(_ config: PlayerConfig) -> Bool { + func prepareNext(_ config: MediaConfig) -> Bool { return false } @@ -146,7 +143,7 @@ class PlayerController: NSObject, Player { self.currentPlayer?.destroy() } - func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (Any) -> Void) { + func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (PKEvent) -> Void) { //Assert.shouldNeverHappen(); } diff --git a/Classes/Player/PlayerDecoratorBase.swift b/Classes/Player/PlayerDecoratorBase.swift index 115159ad..3895a0d3 100644 --- a/Classes/Player/PlayerDecoratorBase.swift +++ b/Classes/Player/PlayerDecoratorBase.swift @@ -61,11 +61,11 @@ public class PlayerDecoratorBase: NSObject, Player { } } - public func prepare(_ config: PlayerConfig) { + public func prepare(_ config: MediaConfig) { return self.player.prepare(config) } - public func prepareNext(_ config: PlayerConfig) -> Bool { + public func prepareNext(_ config: MediaConfig) -> Bool { return self.player.prepareNext(config) } @@ -106,7 +106,7 @@ public class PlayerDecoratorBase: NSObject, Player { return self.player.createPiPController(with: delegate) } - public func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (Any) -> Void) { + 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 1ba89bd3..a9b2f45e 100644 --- a/Classes/Player/PlayerLoader.swift +++ b/Classes/Player/PlayerLoader.swift @@ -22,42 +22,47 @@ class PlayerLoader: PlayerDecoratorBase { var loadedPlugins = Dictionary() var messageBus = MessageBus() - func load(_ config: PlayerConfig) { + func load(pluginConfig: PluginConfig?) { var playerController: PlayerController - if config.mediaEntry != nil { - playerController = PlayerController(mediaEntry: config) - playerController.onEventBlock = { (event:PKEvent) in - self.messageBus.post(event) - } - - // TODO:: - // add event listener on player controller - - var player: Player = playerController - - if let plugins = config.plugins { - for pluginName in plugins.keys { - if let pluginObject = PlayKitManager.sharedInstance.createPlugin(name: pluginName) { - // TODO:: - // send message bus - var decorator: PlayerDecoratorBase? = nil - - pluginObject.load(player: player, mediaConfig: config.mediaEntry!, pluginConfig: plugins[pluginName], messageBus: self.messageBus) - - if let d = (pluginObject as? PlayerDecoratorProvider)?.getPlayerDecorator() { - d.setPlayer(player) - decorator = d - player = d - } - - loadedPlugins[pluginName] = LoadedPlugin(plugin: pluginObject, decorator: decorator) + playerController = PlayerController() + playerController.onEventBlock = { [unowned self] event in + self.messageBus.post(event) + } + + // TODO:: + // add event listener on player controller + + var player: Player = playerController + + if let pluginConfigs = pluginConfig?.config { + for pluginName in pluginConfigs.keys { + let pluginConfig = pluginConfigs[pluginName] + if let pluginObject = PlayKitManager.shared.createPlugin(name: pluginName, player: player, pluginConfig: pluginConfig, messageBus: self.messageBus) { + // TODO:: + // send message bus + var decorator: PlayerDecoratorBase? = nil + + if let d = (pluginObject as? PlayerDecoratorProvider)?.getPlayerDecorator() { + d.setPlayer(player) + decorator = d + player = d } + + loadedPlugins[pluginName] = LoadedPlugin(plugin: pluginObject, decorator: decorator) } } - setPlayer(player) - playerController.prepare(config) } + setPlayer(player) + } + + override func prepare(_ config: MediaConfig) { + // update all loaded plugins with media config + for (pluginName, loadedPlugin) in loadedPlugins { + PKLog.trace("Preparing plugin", pluginName) + loadedPlugin.plugin.onLoad(mediaConfig: config) + } + super.prepare(config) } func destroyPlayer() { @@ -89,7 +94,7 @@ class PlayerLoader: PlayerDecoratorBase { self.destroyPlayer() } - public override func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (_ info: Any)->Void) { + public override func addObserver(_ observer: AnyObject, events: [PKEvent.Type], block: @escaping (PKEvent)->Void) { // TODO:: finilizing + object validation messageBus.addObserver(observer, events: events, block: block) } @@ -98,4 +103,5 @@ class PlayerLoader: PlayerDecoratorBase { // TODO:: finilizing + object validation messageBus.removeObserver(observer, events: events) } + } diff --git a/Classes/Player/PlayerView.swift b/Classes/Player/PlayerView.swift index e435d790..52df6090 100644 --- a/Classes/Player/PlayerView.swift +++ b/Classes/Player/PlayerView.swift @@ -32,5 +32,9 @@ class PlayerView: UIView { self.playerLayer?.frame = CGRect(origin: CGPoint.zero, size: frame.size) } } - + + override func layoutSubviews() { + super.layoutSubviews() + self.playerLayer?.frame = CGRect(origin: CGPoint.zero, size: frame.size) + } } diff --git a/Classes/Player/TracksManager.swift b/Classes/Player/TracksManager.swift index b246f57b..a6452bcf 100644 --- a/Classes/Player/TracksManager.swift +++ b/Classes/Player/TracksManager.swift @@ -11,6 +11,8 @@ import AVFoundation class TracksManager: NSObject { let audioTypeKey: String = "soun" + let textOffDisplay: String = "Off" + private var audioTracks: [Track]? private var textTracks: [Track]? @@ -59,8 +61,14 @@ class TracksManager: NSObject { } public func currentTextTrack(item: AVPlayerItem) -> String? { - if let group = item.asset.mediaSelectionGroup(forMediaCharacteristic: AVMediaCharacteristicLegible), let option = item.selectedMediaOption(in: group) { - return self.textTracks?.filter{($0.title! == option.displayName)}.first?.id + if let group = item.asset.mediaSelectionGroup(forMediaCharacteristic: AVMediaCharacteristicLegible) { + var displayName: String + if let option = item.selectedMediaOption(in: group) { + displayName = option.displayName + } else { + displayName = textOffDisplay + } + return self.textTracks?.filter{($0.title! == displayName)}.first?.id } return nil } @@ -126,7 +134,7 @@ class TracksManager: NSObject { self.textTracks?.append(track) } if optionMediaType != "" { - self.textTracks?.insert(Track(id: "\(optionMediaType):-1", title: "Off", language: nil), at: 0) + self.textTracks?.insert(Track(id: "\(optionMediaType):-1", title: textOffDisplay, language: nil), at: 0) } } diff --git a/Classes/PlayerEvent.swift b/Classes/PlayerEvent.swift index 120d48da..fd428197 100644 --- a/Classes/PlayerEvent.swift +++ b/Classes/PlayerEvent.swift @@ -7,156 +7,165 @@ // import Foundation -public class PlayerEvents: PKEvent { +/// An PlayerEvent is a class used to reflect player events. +public class PlayerEvent: PKEvent { // All events EXCLUDING error. Assuming error events are treated differently. - public static let allEventTypes: [PlayerEvents.Type] = [ - canPlay.self, durationChange.self, ended.self, loadedMetadata.self, - play.self, pause.self, playing.self, seeking.self, seeked.self, stateChanged.self + public static let allEventTypes: [PlayerEvent.Type] = [ + canPlay, durationChanged, ended, loadedMetadata, + play, pause, playing, seeking, seeked, stateChanged, + tracksAvailable, playbackParamsUpdated, error ] - /** - Sent when enough data is available that the media can be played, at least for a couple of frames. - */ - public class canPlay : PlayerEvents {} - /** - The metadata has loaded or changed, indicating a change in duration of the media. This is sent, for example, when the media has loaded enough that the duration is known. - */ - public class durationChange : PlayerEvents { - public var duration: TimeInterval - - init(duration: TimeInterval) { - self.duration = duration + // MARK: - Player Events Static Reference + + /// Sent when enough data is available that the media can be played, at least for a couple of frames. + public static let canPlay: PlayerEvent.Type = CanPlay.self + /// The metadata has loaded or changed, indicating a change in duration of the media. This is sent, for example, when the media has loaded enough that the duration is known. + public static let durationChanged: PlayerEvent.Type = DurationChanged.self + /// Sent when playback completes. + public static let ended: PlayerEvent.Type = Ended.self + /// The media's metadata has finished loading; all attributes now contain as much useful information as they're going to. + public static let loadedMetadata: PlayerEvent.Type = LoadedMetadata.self + /// Sent when an error occurs. + public static let error: PlayerEvent.Type = Error.self + /// Sent when playback of the media starts after having been paused; that is, when playback is resumed after a prior pause event. + public static let play: PlayerEvent.Type = Play.self + /// Sent when playback is paused. + public static let pause: PlayerEvent.Type = Pause.self + /// Sent when the media begins to play (either for the first time, after having been paused, or after ending and then restarting). + public static let playing: PlayerEvent.Type = Playing.self + /// Sent when a seek operation begins. + public static let seeking: PlayerEvent.Type = Seeking.self + /// Sent when a seek operation completes. + public static let seeked: PlayerEvent.Type = Seeked.self + /// Sent when tracks available. + public static let tracksAvailable: PlayerEvent.Type = TracksAvailable.self + /// Sent when Playback Params Updated. + public static let playbackParamsUpdated: PlayerEvent.Type = PlaybackParamsUpdated.self + /// Sent when player state is changed. + public static let stateChanged: PlayerEvent.Type = StateChanged.self + + // MARK: - Player Basic Events + + class CanPlay : PlayerEvent {} + class DurationChanged : PlayerEvent { + convenience init(duration: TimeInterval) { + self.init([EventDataKeys.Duration : NSNumber(value: duration)]) } } - /** - Sent when playback completes. - */ - public class ended : PlayerEvents {} - /** - The media's metadata has finished loading; all attributes now contain as much useful information as they're going to. - */ - public class loadedMetadata : PlayerEvents {} - /** - Sent when an error occurs. - */ - public class error : PlayerEvents {} - /** - Sent when playback of the media starts after having been paused; that is, when playback is resumed after a prior pause event. - */ - public class play : PlayerEvents {} - /** - Sent when playback is paused. - */ - public class pause : PlayerEvents {} - /** - Sent when the media begins to play (either for the first time, after having been paused, or after ending and then restarting). - */ - public class playing : PlayerEvents {} + class Ended : PlayerEvent {} + class LoadedMetadata : PlayerEvent {} + class Play : PlayerEvent {} + class Pause : PlayerEvent {} + class Playing : PlayerEvent {} + class Seeking : PlayerEvent {} + class Seeked : PlayerEvent {} + + class Error: PlayerEvent { + convenience init(error: NSError) { + self.init([EventDataKeys.Error : error]) + } + } - /** - Sent when a seek operation begins. - */ - public class seeking : PlayerEvents {} - - /** - Sent when a seek operation completes. - */ - public class seeked : PlayerEvents {} + // MARK: - Player Tracks Events - /** - Sent when tracks available. - */ - public class tracksAvailable : PlayerEvents { - public var tracks: PKTracks - - public init(tracks: PKTracks) { - self.tracks = tracks + class TracksAvailable : PlayerEvent { + convenience init(tracks: PKTracks) { + self.init([EventDataKeys.Tracks : tracks]) } } - /** - Sent when Playback Params Updated. - */ - public class playbackParamsUpdated : PlayerEvents { - public var currentBitrate: Double - - init(currentBitrate: Double) { - self.currentBitrate = currentBitrate + class PlaybackParamsUpdated : PlayerEvent { + convenience init(currentBitrate: Double) { + self.init([EventDataKeys.CurrentBitrate : NSNumber(value: currentBitrate)]) } } + + // MARK: - Player State Events - /** - Sent when player state is changed. - */ - public class stateChanged : PlayerEvents { - public var newSate: PlayerState - public var oldSate: PlayerState - - public init(newState: PlayerState, oldState: PlayerState) { - self.newSate = newState - self.oldSate = oldState + class StateChanged : PlayerEvent { + convenience init(newState: PlayerState, oldState: PlayerState) { + self.init([EventDataKeys.NewState : newState as AnyObject, + EventDataKeys.OldState : oldState as AnyObject]) } } } -public class AdEvents: PKEvent { - - public static let allEventTypes: [AdEvents.Type] = [ - adBreakReady.self, adBreakEnded.self, adBreakStarted.self, adAllCompleted.self, adComplete.self, adClicked.self, adCuepointsChanged.self, adFirstQuartile.self, adLoaded.self, adLog.self, adMidpoint.self, adPaused.self, adResumed.self, adSkipped.self, adStarted.self, adStreamLoaded.self, adTapped.self, adThirdQuartile.self, adDidProgressToTime.self, adDidRequestPause.self, adDidRequestResume.self, adWebOpenerWillOpenExternalBrowser.self, adWebOpenerWillOpenInAppBrowser.self, adWebOpenerDidOpenInAppBrowser.self, adWebOpenerWillCloseInAppBrowser.self, adWebOpenerDidCloseInAppBrowser.self +// MARK: - Ad Events + +public class AdEvent: PKEvent { + public static let allEventTypes: [AdEvent.Type] = [ + adBreakReady, adBreakEnded, adBreakStarted, adAllCompleted, adComplete, adClicked, adCuepointsChanged, adFirstQuartile, adLoaded, adLog, adMidpoint, adPaused, adResumed, adSkipped, adStarted, adStreamLoaded, adTapped, adThirdQuartile, adDidProgressToTime, adDidRequestPause, adDidRequestResume, adWebOpenerWillOpenExternalBrowser, adWebOpenerWillOpenInAppBrowser, adWebOpenerDidOpenInAppBrowser, adWebOpenerWillCloseInAppBrowser, adWebOpenerDidCloseInAppBrowser ] - public class adBreakReady : AdEvents {} - public class adBreakEnded : AdEvents {} - public class adBreakStarted : AdEvents {} - public class adAllCompleted : AdEvents {} - public class adComplete : AdEvents {} - public class adClicked : AdEvents {} - public class adCuepointsChanged : AdEvents {} - public class adFirstQuartile : AdEvents {} - public class adLoaded : AdEvents {} - public class adLog : AdEvents {} - public class adMidpoint : AdEvents {} - public class adPaused : AdEvents {} - public class adResumed : AdEvents {} - public class adSkipped : AdEvents {} - public class adStarted : AdEvents {} - public class adStreamLoaded : AdEvents {} - public class adTapped : AdEvents {} - public class adThirdQuartile : AdEvents {} + public static let adBreakReady: AdEvent.Type = AdBreakReady.self + public static let adBreakEnded: AdEvent.Type = AdBreakEnded.self + public static let adBreakStarted: AdEvent.Type = AdBreakStarted.self + public static let adAllCompleted: AdEvent.Type = AdAllCompleted.self + public static let adComplete: AdEvent.Type = AdComplete.self + public static let adClicked: AdEvent.Type = AdClicked.self + public static let adCuepointsChanged: AdEvent.Type = AdCuepointsChanged.self + public static let adFirstQuartile: AdEvent.Type = AdFirstQuartile.self + public static let adLoaded: AdEvent.Type = AdLoaded.self + public static let adLog: AdEvent.Type = AdLog.self + public static let adMidpoint: AdEvent.Type = AdMidpoint.self + public static let adPaused: AdEvent.Type = AdPaused.self + public static let adResumed: AdEvent.Type = AdResumed.self + public static let adSkipped: AdEvent.Type = AdSkipped.self + public static let adStarted: AdEvent.Type = AdStarted.self + public static let adStreamLoaded: AdEvent.Type = AdStreamLoaded.self + public static let adTapped: AdEvent.Type = AdTapped.self + public static let adThirdQuartile: AdEvent.Type = AdThirdQuartile.self + public static let adDidProgressToTime: AdEvent.Type = AdDidProgressToTime.self + public static let adDidRequestPause: AdEvent.Type = AdDidRequestPause.self + public static let adDidRequestResume: AdEvent.Type = AdDidRequestResume.self + public static let webOpenerEvent: AdEvent.Type = WebOpenerEvent.self + public static let adWebOpenerWillOpenExternalBrowser: AdEvent.Type = AdWebOpenerWillOpenExternalBrowser.self + public static let adWebOpenerWillOpenInAppBrowser: AdEvent.Type = AdWebOpenerWillOpenInAppBrowser.self + public static let adWebOpenerDidOpenInAppBrowser: AdEvent.Type = AdWebOpenerDidOpenInAppBrowser.self + public static let adWebOpenerWillCloseInAppBrowser: AdEvent.Type = AdWebOpenerWillCloseInAppBrowser.self + public static let adWebOpenerDidCloseInAppBrowser: AdEvent.Type = AdWebOpenerDidCloseInAppBrowser.self - public class adDidProgressToTime : AdEvents { - public let mediaTime, totalTime: TimeInterval - init(mediaTime: TimeInterval, totalTime: TimeInterval) { - self.mediaTime = mediaTime - self.totalTime = totalTime - } - - public required init() { - fatalError("init() has not been implemented") + class AdBreakReady : AdEvent {} + class AdBreakEnded : AdEvent {} + class AdBreakStarted : AdEvent {} + class AdAllCompleted : AdEvent {} + class AdComplete : AdEvent {} + class AdClicked : AdEvent {} + class AdCuepointsChanged : AdEvent {} + class AdFirstQuartile : AdEvent {} + class AdLoaded : AdEvent {} + class AdLog : AdEvent {} + class AdMidpoint : AdEvent {} + class AdPaused : AdEvent {} + class AdResumed : AdEvent {} + class AdSkipped : AdEvent {} + class AdStarted : AdEvent {} + class AdStreamLoaded : AdEvent {} + class AdTapped : AdEvent {} + class AdThirdQuartile : AdEvent {} + + class AdDidProgressToTime : AdEvent { + convenience init(mediaTime: TimeInterval, totalTime: TimeInterval) { + self.init([AdEventDataKeys.MediaTime: NSNumber(value: mediaTime), + AdEventDataKeys.TotalTime: NSNumber(value: totalTime)]) } } - public class adDidRequestPause : AdEvents {} - public class adDidRequestResume : AdEvents {} + + class AdDidRequestPause : AdEvent {} + class AdDidRequestResume : AdEvent {} - public class WebOpenerEvent : AdEvents { - let webOpener: NSObject - public init(webOpener: NSObject!) { - self.webOpener = webOpener - } - public required init() { - fatalError("init() has not been implemented") + class WebOpenerEvent : AdEvent { + convenience init(webOpener: NSObject!) { + self.init([AdEventDataKeys.WebOpener: webOpener]) } } - public class adWebOpenerWillOpenExternalBrowser : WebOpenerEvent {} - public class adWebOpenerWillOpenInAppBrowser : WebOpenerEvent {} - public class adWebOpenerDidOpenInAppBrowser : WebOpenerEvent {} - public class adWebOpenerWillCloseInAppBrowser : WebOpenerEvent {} - public class adWebOpenerDidCloseInAppBrowser : WebOpenerEvent {} - - - - public required override init() {} + class AdWebOpenerWillOpenExternalBrowser : WebOpenerEvent {} + class AdWebOpenerWillOpenInAppBrowser : WebOpenerEvent {} + class AdWebOpenerDidOpenInAppBrowser : WebOpenerEvent {} + class AdWebOpenerWillCloseInAppBrowser : WebOpenerEvent {} + class AdWebOpenerDidCloseInAppBrowser : WebOpenerEvent {} } diff --git a/Classes/PlayerState.swift b/Classes/PlayerState.swift index 8a9b79c4..4095b9cd 100644 --- a/Classes/PlayerState.swift +++ b/Classes/PlayerState.swift @@ -8,10 +8,18 @@ import Foundation -public enum PlayerState { +/// An PlayerState is an enum of different player states +@objc public enum PlayerState: Int { +/// Sent when player's state idle. case idle +/// Sent when player's state loading. case loading +/// Sent when player's state ready. case ready +/// Sent when player's state buffering. case buffering +/// Sent when player's state errored. case error +/// Sent when player's state unknown. + case unknown = -1 } diff --git a/Classes/Plugins/Ads/AdsConfig.swift b/Classes/Plugins/Ads/AdsConfig.swift index f69639a0..088d6b20 100644 --- a/Classes/Plugins/Ads/AdsConfig.swift +++ b/Classes/Plugins/Ads/AdsConfig.swift @@ -8,7 +8,7 @@ import Foundation -public class AdsConfig { +public class AdsConfig: NSObject { public var language: String = "en" public var enableBackgroundPlayback: Bool { get { @@ -26,11 +26,7 @@ public class AdsConfig { public var tagsTimes: [TimeInterval : String]? public var companionView: UIView? public var webOpenerPresentingController: UIViewController? - - public init() { - - } - + // Builders @discardableResult public func set(language: String) -> Self { diff --git a/Classes/Plugins/Ads/AdsEnabledPlayerController.swift b/Classes/Plugins/Ads/AdsEnabledPlayerController.swift index 746ca20e..b56688a2 100644 --- a/Classes/Plugins/Ads/AdsEnabledPlayerController.swift +++ b/Classes/Plugins/Ads/AdsEnabledPlayerController.swift @@ -90,13 +90,13 @@ class AdsEnabledPlayerController : PlayerDecoratorBase, AdsPluginDelegate, AdsPl } func adsPlugin(_ adsPlugin: AdsPlugin, didReceive event: PKEvent) { - if event is AdEvents.adDidRequestPause { + if event is AdEvent.AdDidRequestPause { super.pause() self.isAdPlayback = true - } else if event is AdEvents.adDidRequestResume { + } else if event is AdEvent.AdDidRequestResume { super.play() self.isAdPlayback = false - } else if event is AdEvents.adResumed { + } else if event is AdEvent.AdResumed { self.isPlayEnabled = true } } diff --git a/Classes/Plugins/Analytics/AnalyticsConfig.swift b/Classes/Plugins/Analytics/AnalyticsConfig.swift index 174891fd..c8a504ff 100644 --- a/Classes/Plugins/Analytics/AnalyticsConfig.swift +++ b/Classes/Plugins/Analytics/AnalyticsConfig.swift @@ -9,10 +9,8 @@ import UIKit public class AnalyticsConfig { - public var mediaEntry: MediaEntry? - public var params: [String: Any]! - public init() { - - } + public var params = [String : Any]() + + public init() {} } diff --git a/Classes/Plugins/PKPlugin.swift b/Classes/Plugins/PKPlugin.swift index d42d1070..d713c10a 100644 --- a/Classes/Plugins/PKPlugin.swift +++ b/Classes/Plugins/PKPlugin.swift @@ -9,13 +9,30 @@ import UIKit import AVFoundation -@objc public protocol PKPlugin { - +/** + Used as a workaround for Apple bug with swift interoperability. + + There is an issue with initializing an object based on a protocol.Type with @objc attribute. + Therefore we use a wrapper protocol for PKPlugin with @objc and then casting to a PKPlugin without the @objc attribute. + + - important: + **should not be used! use PKPlugin to add a plugin** + */ +@objc public protocol Plugin {} + +/// The `PKPlugin` protocol defines all the properties and methods required to define a plugin object. +public protocol PKPlugin: Plugin { + /// The plugin name. static var pluginName: String { get } + /// The associated media entry. + weak var mediaEntry: MediaEntry? { get set } - init() - - func load(player: Player, mediaConfig: MediaEntry, pluginConfig: Any?, messageBus: MessageBus) + init(player: Player, pluginConfig: Any?, messageBus: MessageBus) + /// 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 + func onUpdateMedia(mediaConfig: MediaConfig) func destroy() } + diff --git a/Classes/Providers/OTT/Session/OTTSessionManager.swift b/Classes/Providers/OTT/Session/OTTSessionManager.swift index 607595c2..9b2801b1 100644 --- a/Classes/Providers/OTT/Session/OTTSessionManager.swift +++ b/Classes/Providers/OTT/Session/OTTSessionManager.swift @@ -99,7 +99,7 @@ public class OTTSessionManager: SessionProvider { } } - public func startAnonymouseSession(completion:@escaping (_ error:Error?)->Void) { + public func startAnonymousSession(completion:@escaping (_ error:Error?)->Void) { let loginRequestBuilder = OTTUserService.anonymousLogin(baseURL: self.serverURL, partnerId: self.partnerId) diff --git a/Classes/Providers/OVP/Model/OVPMetadata.swift b/Classes/Providers/OVP/Model/OVPMetadata.swift new file mode 100644 index 00000000..aa3b2f02 --- /dev/null +++ b/Classes/Providers/OVP/Model/OVPMetadata.swift @@ -0,0 +1,22 @@ +// +// OVPMetadata.swift +// Pods +// +// Created by Itay Kinnrot on 09/01/2017. +// +// + +import UIKit +import SwiftyJSON + +class OVPMetadata: OVPBaseObject { + var xml:String? + + required init?(json: Any) { + + let jsonObject = JSON(json) + self.xml = jsonObject["xml"].string + + } + +} diff --git a/Classes/Providers/OVP/Model/OVPSource.swift b/Classes/Providers/OVP/Model/OVPSource.swift index f2927841..2aaf0f3e 100644 --- a/Classes/Providers/OVP/Model/OVPSource.swift +++ b/Classes/Providers/OVP/Model/OVPSource.swift @@ -45,7 +45,7 @@ class OVPSource: OVPBaseObject { self.flavors = flavors.components(separatedBy: ",") } - if let url = jsonObject[urlKey].URL{ + if let url = jsonObject[urlKey].url{ self.url = url } diff --git a/Classes/Providers/OVP/OVPMediaProvider.swift b/Classes/Providers/OVP/OVPMediaProvider.swift index 5f26186e..e1babad9 100644 --- a/Classes/Providers/OVP/OVPMediaProvider.swift +++ b/Classes/Providers/OVP/OVPMediaProvider.swift @@ -7,8 +7,11 @@ // import UIKit +import SwiftyXMLParser public class OVPMediaProvider: MediaEntryProvider { + + //This object is initiate at the begning of loadMedia methos and contain all neccessery info to load. struct LoaderInfo { @@ -35,6 +38,11 @@ public class OVPMediaProvider: MediaEntryProvider { public init(){} + + public init(_ sessionProvider: SessionProvider) { + self.set(sessionProvider: sessionProvider) + } + /** session provider - which resposible for the ks, prtner id, and base server url */ @@ -144,9 +152,10 @@ public class OVPMediaProvider: MediaEntryProvider { let getPlaybackContext = OVPBaseEntryService.getPlaybackContext(baseURL: loadInfo.apiServerURL, ks: token, entryID: loadInfo.entryId) + let metadataRequest = OVPBaseEntryService.metadata(baseURL: loadInfo.apiServerURL, ks: token, entryID: loadInfo.entryId) guard let req1 = listRequest, - let req2 = getPlaybackContext else { + let req2 = getPlaybackContext, let req3 = metadataRequest else { callback(Result(data: nil, error: Err.invalidParams)) return } @@ -154,6 +163,7 @@ public class OVPMediaProvider: MediaEntryProvider { //Building the multi request mrb?.add(request: req1) .add(request: req2) + .add(request: req3) .set(completion: { (dataResponse:Response) in let responses: [OVPBaseObject] = OVPMultiResponseParser.parse(data: dataResponse.data) @@ -166,14 +176,17 @@ public class OVPMediaProvider: MediaEntryProvider { return } - let mainResponse: OVPBaseObject = responses[responses.count-2] - let contextDataResponse: OVPBaseObject = responses[responses.count-1] + let metaData:OVPBaseObject = responses[responses.count-1] + let contextDataResponse: OVPBaseObject = responses[responses.count-2] + let mainResponse: OVPBaseObject = responses[responses.count-3] guard let mainResponseData = mainResponse as? OVPList, let entry = mainResponseData.objects?.last as? OVPEntry, let contextData = contextDataResponse as? OVPPlaybackContext, - let sources = contextData.sources + let sources = contextData.sources, + let metadataListObject = metaData as? OVPList, + let metadataList = metadataListObject.objects as? [OVPMetadata] else{ callback(Result(data: nil, error: Err.invalidResponse )) PKLog.debug("Response is not containing Entry info or playback data") @@ -213,12 +226,14 @@ public class OVPMediaProvider: MediaEntryProvider { mediaSources.append(mediaSource) }) + let metaDataItems = self.getMetadata(metadataList: metadataList) + //creating media entry with the above sources let mediaEntry: MediaEntry = MediaEntry(id: entry.id) mediaEntry.duration = entry.duration mediaEntry.sources = mediaSources + mediaEntry.metadata = metaDataItems callback(Result(data: mediaEntry, error: nil )) - }) @@ -232,6 +247,29 @@ public class OVPMediaProvider: MediaEntryProvider { } + private func getMetadata(metadataList:[OVPMetadata])->[String:String] { + var metaDataItems = [String: String]() + + for meta in metadataList { + do{ + if let metaXML = meta.xml { + let xml = try XML.parse(metaXML) + if let allNodes = xml["metadata"].all{ + for element in allNodes { + for dataElement in element.childElements { + metaDataItems[dataElement.name] = dataElement.text + } + } + } + } + }catch{ + PKLog.warning("Error occur while trying to parse metadata XML") + } + } + + return metaDataItems + } + // This method decding the source type base on scheck and drm data private func getSourceType(source:OVPSource) -> MediaSource.SourceType { @@ -271,7 +309,7 @@ public class OVPMediaProvider: MediaEntryProvider { var drmData: DRMData? = nil switch scheme { - case "fps": + case "fairplay.FAIRPLAY": guard let certifictae = drm.certificate, let licenseURL = drm.licenseURL // if the scheme is type fair play and there is no certificate or license URL diff --git a/Classes/Providers/OVP/Parsers/OVPObjectMapper.swift b/Classes/Providers/OVP/Parsers/OVPObjectMapper.swift index 83401708..953d0b99 100644 --- a/Classes/Providers/OVP/Parsers/OVPObjectMapper.swift +++ b/Classes/Providers/OVP/Parsers/OVPObjectMapper.swift @@ -29,6 +29,8 @@ class OVPObjectMapper: NSObject { return OVPError.self case "KalturaStartWidgetSessionResponse": return OVPStartWidgetSessionResponse.self + case "KalturaMetadata": + return OVPMetadata.self default: return nil } diff --git a/Classes/Providers/OVP/Services/OVPBaseEntryService.swift b/Classes/Providers/OVP/Services/OVPBaseEntryService.swift index 873e86d1..b3da5da1 100644 --- a/Classes/Providers/OVP/Services/OVPBaseEntryService.swift +++ b/Classes/Providers/OVP/Services/OVPBaseEntryService.swift @@ -25,6 +25,19 @@ class OVPBaseEntryService { return nil } } + + internal static func metadata(baseURL: String, ks: String,entryID: String) -> KalturaRequestBuilder? { + + if let request: KalturaRequestBuilder = KalturaRequestBuilder(url: baseURL, service: "metadata_metadata", action: "list") { + request.setBody(key: "ks", value: JSON(ks)) + .setBody(key: "filter:objectType", value: JSON("KalturaMetadataFilter")) + .setBody(key: "filter:objectIdEqual", value: JSON(entryID)) + .setBody(key: "filter:metadataObjectTypeEqual", value: JSON("1")) + return request + }else{ + return nil + } + } internal static func getContextData(baseURL: String, ks: String,entryID: String) -> KalturaRequestBuilder? { diff --git a/Classes/Providers/OVP/Session/OVPSessionManager.swift b/Classes/Providers/OVP/Session/OVPSessionManager.swift index d8c38ae6..72794a5c 100644 --- a/Classes/Providers/OVP/Session/OVPSessionManager.swift +++ b/Classes/Providers/OVP/Session/OVPSessionManager.swift @@ -41,12 +41,10 @@ public class OVPSessionManager: SessionProvider { private let defaultSessionExpiry = TimeInterval(24*60*60) - - public init(serverURL: String, version:String, partnerId: Int64, executor: RequestExecutor?) { - + public init(serverURL: String, partnerId: Int64, executor: RequestExecutor? = nil) { self.serverURL = serverURL self.partnerId = partnerId - self.version = version + self.version = "api_v3" self.fullServerPath = self.serverURL.appending("/\(self.version)") if let exe = executor { @@ -55,6 +53,11 @@ public class OVPSessionManager: SessionProvider { self.executor = USRExecutor.shared } } + + @available(*, deprecated, message: "Use init(serverURL:partnerId:executor:)") + public convenience init(serverURL: String, version:String, partnerId: Int64, executor: RequestExecutor?) { + self.init(serverURL: serverURL, partnerId: partnerId, executor: executor) + } public func loadKS(completion: @escaping (_ result :Result) -> Void){ if let ks = self.ks, self.tokenExpiration?.compare(Date()) == ComparisonResult.orderedDescending { @@ -72,7 +75,7 @@ public class OVPSessionManager: SessionProvider { } else{ - self.startAnonymouseSession(completion: { (e:Error?) in + self.startAnonymousSession(completion: { (e:Error?) in self.ensureKSAfterRefresh(e: e, completion: completion) }) } @@ -94,10 +97,7 @@ public class OVPSessionManager: SessionProvider { } - - - - public func startAnonymouseSession(completion:@escaping (_ error:Error?)->Void) -> Void { + public func startAnonymousSession(completion:@escaping (_ error:Error?)->Void) -> Void { let loginRequestBuilder = OVPSessionService.startWidgetSession(baseURL: self.fullServerPath, partnerId: self.partnerId)? diff --git a/Classes/Providers/OVP/SimpleOVPSessionProvider.swift b/Classes/Providers/OVP/SimpleOVPSessionProvider.swift new file mode 100644 index 00000000..f36d2c56 --- /dev/null +++ b/Classes/Providers/OVP/SimpleOVPSessionProvider.swift @@ -0,0 +1,43 @@ +// +// Created by Noam Tamim on 09/02/2017. +// +// + +import UIKit + +/** + A SessionProvider that just reflects its input parameters -- baseUrl, partnerId, ks. + Unlike the full OVPSessionManager, this class does not attempt to manage (create, renew, validate, clear) a session. + The application is expected to provide a valid KS, which it can update as required via the `ks` property. For some + use cases, the KS can be null (anonymous media playback, if allowed by access-control). Basic usage with a OVPMediaProvider: + + let mediaProvider = OVPMediaProvider(SimpleOVPSessionProvider(serverURL: "https://cdnapisec.kaltura.com", + partnerId: 1851571, + ks: applicationKS)) + mediaProvider.set(entryId: "0_pl5lbfo0").loadMedia { (entry) in + print("entry:", entry.data ?? "") + } + + */ +public class SimpleOVPSessionProvider: SessionProvider { + public let serverURL: String + public let partnerId: Int64 + public var ks: String? + + /** + Build an OVP SessionProvider with the specified parameters. + - Parameters: + - serverURL: Kaltura Server URL, such as `"https://cdnapisec.kaltura.com"`. + - partnerId: Kaltura partner id. + - ks: Kaltura Session token. + */ + public init(serverURL: String, partnerId: Int64, ks: String?) { + self.serverURL = serverURL + self.partnerId = partnerId + self.ks = ks + } + + public func loadKS(completion: @escaping (Result) -> Void) { + completion(Result(data: ks, error: nil)) + } +} diff --git a/Example/PlayKit.xcodeproj/project.pbxproj b/Example/PlayKit.xcodeproj/project.pbxproj index c964ad13..5733667a 100644 --- a/Example/PlayKit.xcodeproj/project.pbxproj +++ b/Example/PlayKit.xcodeproj/project.pbxproj @@ -7,12 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + 49AD556D5A90D82F391750B8 /* Pods_PlayKit_PlayKit_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9BF84E8D861C5637BA8624EE /* Pods_PlayKit_PlayKit_Example.framework */; }; 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; }; 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; - 68CB7DDFACCC26CDA5B7D2C5 /* Pods_PlayKit_PlayKit_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF5A76DC5911C10F6FD4A456 /* Pods_PlayKit_PlayKit_Example.framework */; }; + 608BDC243A1EB15785E47C80 /* Pods_PlayKit_PlayKit_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE76A2A9EC2DDAC016A40200 /* Pods_PlayKit_PlayKit_Tests.framework */; }; 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 */; }; @@ -22,9 +23,8 @@ 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 */; }; - C28432D81E23B59B003B385D /* GoogleCastTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28432D71E23B59B003B385D /* GoogleCastTest.swift */; }; - C34F64BC72D3DC74C0ED0E72 /* Pods_PlayKit_PlayKit_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CF530277CFF9F380CF3D1EE9 /* Pods_PlayKit_PlayKit_Tests.framework */; }; 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 */ @@ -38,17 +38,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 03311B039DA45D2A6638063F /* Pods-PlayKit-PlayKit_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit-PlayKit_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit-PlayKit_Tests/Pods-PlayKit-PlayKit_Tests.debug.xcconfig"; sourceTree = ""; }; - 13E67B5C9AB81A0AD2F442FC /* Pods-PlayKit-PlayKit_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit-PlayKit_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit-PlayKit_Example/Pods-PlayKit-PlayKit_Example.debug.xcconfig"; sourceTree = ""; }; - 144D2DCFFD858745D8B4B99B /* Pods-PlayKit_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit_Tests/Pods-PlayKit_Tests.release.xcconfig"; sourceTree = ""; }; - 1831F29EEB34ADBEAA5D9C0C /* Pods-PlayKit_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit_Example/Pods-PlayKit_Example.debug.xcconfig"; sourceTree = ""; }; - 2F3B38AA108FA3B99649CE4C /* Pods_PlayKit_Example_PlayKit_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PlayKit_Example_PlayKit_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 347179E45954932C0B17A1AF /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 349AFD3D2D9F85463A6A0F41 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; - 365E7439B9DEAEEC8A0B4D91 /* Pods-PlayKit-PlayKit_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit-PlayKit_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit-PlayKit_Example/Pods-PlayKit-PlayKit_Example.release.xcconfig"; sourceTree = ""; }; - 378E498F96C5AFC15C8789E8 /* Pods-PlayKit_Example-PlayKit_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit_Example-PlayKit_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit_Example-PlayKit_Tests/Pods-PlayKit_Example-PlayKit_Tests.release.xcconfig"; sourceTree = ""; }; - 44B4B9554FF14CFCA4398271 /* Pods-PlayKit_Example-PlayKit_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit_Example-PlayKit_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit_Example-PlayKit_Tests/Pods-PlayKit_Example-PlayKit_Tests.debug.xcconfig"; sourceTree = ""; }; 45A6ED5153A41479610084F1 /* PlayKit.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = PlayKit.podspec; path = ../PlayKit.podspec; sourceTree = ""; }; + 5C6C1B5684E828DF03386E55 /* Pods-PlayKit-PlayKit_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit-PlayKit_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit-PlayKit_Example/Pods-PlayKit-PlayKit_Example.release.xcconfig"; sourceTree = ""; }; 607FACD01AFB9204008FA782 /* PlayKit_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PlayKit_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -59,7 +52,11 @@ 607FACE51AFB9204008FA782 /* PlayKit_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PlayKit_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8460889B1DD1AB1A009E0E7A /* Entries.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Entries.json; sourceTree = ""; }; - B50CE31D283C9191A9093986 /* Pods-PlayKit-PlayKit_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit-PlayKit_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit-PlayKit_Tests/Pods-PlayKit-PlayKit_Tests.release.xcconfig"; sourceTree = ""; }; + 94FC39BB3975D26F28B5A473 /* Pods-PlayKit-PlayKit_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit-PlayKit_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit-PlayKit_Example/Pods-PlayKit-PlayKit_Example.debug.xcconfig"; sourceTree = ""; }; + 9BF84E8D861C5637BA8624EE /* Pods_PlayKit_PlayKit_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PlayKit_PlayKit_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9EB557AA5D4017F3D9667F25 /* Pods-PlayKit-PlayKit_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit-PlayKit_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit-PlayKit_Tests/Pods-PlayKit-PlayKit_Tests.debug.xcconfig"; sourceTree = ""; }; + B6357392C349F8B33892AE33 /* Pods-PlayKit-PlayKit_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit-PlayKit_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit-PlayKit_Tests/Pods-PlayKit-PlayKit_Tests.release.xcconfig"; sourceTree = ""; }; + 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 = ""; }; @@ -67,14 +64,8 @@ C23A62CF1DF4131700635FA2 /* MediaEntryProviderMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaEntryProviderMock.swift; sourceTree = ""; }; C23A62DB1DF47D9800635FA2 /* OTTSessionProviderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTTSessionProviderTest.swift; sourceTree = ""; }; C24770AC1DEDE72F00E37C89 /* ovp.multirequest._.1_1h1vsv3z.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ovp.multirequest._.1_1h1vsv3z.json; sourceTree = ""; }; - C28432D71E23B59B003B385D /* GoogleCastTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleCastTest.swift; sourceTree = ""; }; - C6039FAD64FBD55034BCB260 /* Pods-PlayKit_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit_Tests/Pods-PlayKit_Tests.debug.xcconfig"; sourceTree = ""; }; C63FA2AE1DF3F854004030E0 /* PlayerControllerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerControllerTest.swift; sourceTree = ""; }; - CF530277CFF9F380CF3D1EE9 /* Pods_PlayKit_PlayKit_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PlayKit_PlayKit_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D1F7DD9C5361102A75AA1CAB /* Pods_PlayKit_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PlayKit_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DF5A76DC5911C10F6FD4A456 /* Pods_PlayKit_PlayKit_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PlayKit_PlayKit_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F0608E8A07C05613B6F7157E /* Pods-PlayKit_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PlayKit_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-PlayKit_Example/Pods-PlayKit_Example.release.xcconfig"; sourceTree = ""; }; - F22FBF5CD0C0446ED67E8399 /* Pods_PlayKit_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PlayKit_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FB09C99B1E28072900D3671F /* SourceSelectorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SourceSelectorTest.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -82,7 +73,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 68CB7DDFACCC26CDA5B7D2C5 /* Pods_PlayKit_PlayKit_Example.framework in Frameworks */, + 49AD556D5A90D82F391750B8 /* Pods_PlayKit_PlayKit_Example.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -90,7 +81,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C34F64BC72D3DC74C0ED0E72 /* Pods_PlayKit_PlayKit_Tests.framework in Frameworks */, + 608BDC243A1EB15785E47C80 /* Pods_PlayKit_PlayKit_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -105,8 +96,8 @@ 607FACD21AFB9204008FA782 /* Example for PlayKit */, 607FACE81AFB9204008FA782 /* Tests */, 607FACD11AFB9204008FA782 /* Products */, - 6D26E26C47EAC447DD34726F /* Pods */, E927D97FFF1E38C30E74E325 /* Frameworks */, + 67D95A4D82926185B070426F /* Pods */, ); sourceTree = ""; }; @@ -144,10 +135,10 @@ 607FACE81AFB9204008FA782 /* Tests */ = { isa = PBXGroup; children = ( - C28432D61E23B57A003B385D /* GoogleCast */, C23A62C81DF412FD00635FA2 /* MediaEntryProvider */, C63FA2AE1DF3F854004030E0 /* PlayerControllerTest.swift */, BF5C80811DF0E36500D3E665 /* MessegeBusTest.swift */, + FB09C99B1E28072900D3671F /* SourceSelectorTest.swift */, 607FACE91AFB9204008FA782 /* Supporting Files */, ); path = Tests; @@ -171,19 +162,13 @@ name = "Podspec Metadata"; sourceTree = ""; }; - 6D26E26C47EAC447DD34726F /* Pods */ = { + 67D95A4D82926185B070426F /* Pods */ = { isa = PBXGroup; children = ( - 1831F29EEB34ADBEAA5D9C0C /* Pods-PlayKit_Example.debug.xcconfig */, - F0608E8A07C05613B6F7157E /* Pods-PlayKit_Example.release.xcconfig */, - C6039FAD64FBD55034BCB260 /* Pods-PlayKit_Tests.debug.xcconfig */, - 144D2DCFFD858745D8B4B99B /* Pods-PlayKit_Tests.release.xcconfig */, - 44B4B9554FF14CFCA4398271 /* Pods-PlayKit_Example-PlayKit_Tests.debug.xcconfig */, - 378E498F96C5AFC15C8789E8 /* Pods-PlayKit_Example-PlayKit_Tests.release.xcconfig */, - 13E67B5C9AB81A0AD2F442FC /* Pods-PlayKit-PlayKit_Example.debug.xcconfig */, - 365E7439B9DEAEEC8A0B4D91 /* Pods-PlayKit-PlayKit_Example.release.xcconfig */, - 03311B039DA45D2A6638063F /* Pods-PlayKit-PlayKit_Tests.debug.xcconfig */, - B50CE31D283C9191A9093986 /* Pods-PlayKit-PlayKit_Tests.release.xcconfig */, + 94FC39BB3975D26F28B5A473 /* Pods-PlayKit-PlayKit_Example.debug.xcconfig */, + 5C6C1B5684E828DF03386E55 /* Pods-PlayKit-PlayKit_Example.release.xcconfig */, + 9EB557AA5D4017F3D9667F25 /* Pods-PlayKit-PlayKit_Tests.debug.xcconfig */, + B6357392C349F8B33892AE33 /* Pods-PlayKit-PlayKit_Tests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -209,22 +194,11 @@ path = MediaEntryProvider; sourceTree = ""; }; - C28432D61E23B57A003B385D /* GoogleCast */ = { - isa = PBXGroup; - children = ( - C28432D71E23B59B003B385D /* GoogleCastTest.swift */, - ); - path = GoogleCast; - sourceTree = ""; - }; E927D97FFF1E38C30E74E325 /* Frameworks */ = { isa = PBXGroup; children = ( - F22FBF5CD0C0446ED67E8399 /* Pods_PlayKit_Example.framework */, - D1F7DD9C5361102A75AA1CAB /* Pods_PlayKit_Tests.framework */, - 2F3B38AA108FA3B99649CE4C /* Pods_PlayKit_Example_PlayKit_Tests.framework */, - DF5A76DC5911C10F6FD4A456 /* Pods_PlayKit_PlayKit_Example.framework */, - CF530277CFF9F380CF3D1EE9 /* Pods_PlayKit_PlayKit_Tests.framework */, + 9BF84E8D861C5637BA8624EE /* Pods_PlayKit_PlayKit_Example.framework */, + BE76A2A9EC2DDAC016A40200 /* Pods_PlayKit_PlayKit_Tests.framework */, ); name = Frameworks; sourceTree = ""; @@ -236,12 +210,12 @@ isa = PBXNativeTarget; buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "PlayKit_Example" */; buildPhases = ( - 5BEED449A2724ADC6E50C6C0 /* [CP] Check Pods Manifest.lock */, + E34578C31E296D0B81446FF1 /* [CP] Check Pods Manifest.lock */, 607FACCC1AFB9204008FA782 /* Sources */, 607FACCD1AFB9204008FA782 /* Frameworks */, 607FACCE1AFB9204008FA782 /* Resources */, - FCF9E8B3BE5D23B71E657746 /* [CP] Embed Pods Frameworks */, - 66939DFEDC6594F0720B854F /* [CP] Copy Pods Resources */, + 7A881257393B54AEAE0C3921 /* [CP] Embed Pods Frameworks */, + CE7C2C52A8072495CF2EA38B /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -256,12 +230,12 @@ isa = PBXNativeTarget; buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "PlayKit_Tests" */; buildPhases = ( - DAC3BE8D58C8F0BCC32285D8 /* [CP] Check Pods Manifest.lock */, + 1F1EB354A76C24C78E02A4D4 /* [CP] Check Pods Manifest.lock */, 607FACE11AFB9204008FA782 /* Sources */, 607FACE21AFB9204008FA782 /* Frameworks */, 607FACE31AFB9204008FA782 /* Resources */, - E320BE104658EDF9FE1AC719 /* [CP] Embed Pods Frameworks */, - 25BD8603F3A4BE4372C26FCD /* [CP] Copy Pods Resources */, + 9540D9F4804C3D964E57CA93 /* [CP] Embed Pods Frameworks */, + 1877AC6D640E4B5F1F63EAAD /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -338,7 +312,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 25BD8603F3A4BE4372C26FCD /* [CP] Copy Pods Resources */ = { + 1877AC6D640E4B5F1F63EAAD /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -353,7 +327,7 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PlayKit-PlayKit_Tests/Pods-PlayKit-PlayKit_Tests-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 5BEED449A2724ADC6E50C6C0 /* [CP] Check Pods Manifest.lock */ = { + 1F1EB354A76C24C78E02A4D4 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -368,64 +342,64 @@ shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; - 66939DFEDC6594F0720B854F /* [CP] Copy Pods Resources */ = { + 7A881257393B54AEAE0C3921 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "[CP] Copy Pods Resources"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PlayKit-PlayKit_Example/Pods-PlayKit-PlayKit_Example-resources.sh\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PlayKit-PlayKit_Example/Pods-PlayKit-PlayKit_Example-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - DAC3BE8D58C8F0BCC32285D8 /* [CP] Check Pods Manifest.lock */ = { + 9540D9F4804C3D964E57CA93 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PlayKit-PlayKit_Tests/Pods-PlayKit-PlayKit_Tests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - E320BE104658EDF9FE1AC719 /* [CP] Embed Pods Frameworks */ = { + CE7C2C52A8072495CF2EA38B /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Copy Pods Resources"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PlayKit-PlayKit_Tests/Pods-PlayKit-PlayKit_Tests-frameworks.sh\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PlayKit-PlayKit_Example/Pods-PlayKit-PlayKit_Example-resources.sh\"\n"; showEnvVarsInLog = 0; }; - FCF9E8B3BE5D23B71E657746 /* [CP] Embed Pods Frameworks */ = { + E34578C31E296D0B81446FF1 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Check Pods Manifest.lock"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PlayKit-PlayKit_Example/Pods-PlayKit-PlayKit_Example-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -449,9 +423,9 @@ C23A62D31DF4140E00635FA2 /* OVPMediaProviederTest.swift in Sources */, C63FA2B01DF3F854004030E0 /* PlayerControllerTest.swift in Sources */, BF5C80821DF0E36500D3E665 /* MessegeBusTest.swift in Sources */, + FB09C99C1E28072900D3671F /* SourceSelectorTest.swift in Sources */, C23A62D21DF4140B00635FA2 /* OTTMediaProviderTest.swift in Sources */, C23A62DD1DF47D9C00635FA2 /* OTTSessionProviderTest.swift in Sources */, - C28432D81E23B59B003B385D /* GoogleCastTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -574,7 +548,7 @@ }; 607FACF01AFB9204008FA782 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 13E67B5C9AB81A0AD2F442FC /* Pods-PlayKit-PlayKit_Example.debug.xcconfig */; + baseConfigurationReference = 94FC39BB3975D26F28B5A473 /* Pods-PlayKit-PlayKit_Example.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEFINES_MODULE = YES; @@ -585,7 +559,7 @@ INFOPLIST_FILE = PlayKit/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MODULE_NAME = ExampleApp; - PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = com.kaltura.playkit; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 3.0; }; @@ -593,7 +567,7 @@ }; 607FACF11AFB9204008FA782 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 365E7439B9DEAEEC8A0B4D91 /* Pods-PlayKit-PlayKit_Example.release.xcconfig */; + baseConfigurationReference = 5C6C1B5684E828DF03386E55 /* Pods-PlayKit-PlayKit_Example.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEFINES_MODULE = YES; @@ -603,7 +577,7 @@ INFOPLIST_FILE = PlayKit/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MODULE_NAME = ExampleApp; - PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = com.kaltura.playkit; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 3.0; }; @@ -611,11 +585,11 @@ }; 607FACF31AFB9204008FA782 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 03311B039DA45D2A6638063F /* Pods-PlayKit-PlayKit_Tests.debug.xcconfig */; + baseConfigurationReference = 9EB557AA5D4017F3D9667F25 /* Pods-PlayKit-PlayKit_Tests.debug.xcconfig */; buildSettings = { DEPLOYMENT_POSTPROCESSING = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", @@ -623,7 +597,6 @@ GCC_SYMBOLS_PRIVATE_EXTERN = YES; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 3.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PlayKit_Example.app/PlayKit_Example"; @@ -633,14 +606,13 @@ }; 607FACF41AFB9204008FA782 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B50CE31D283C9191A9093986 /* Pods-PlayKit-PlayKit_Tests.release.xcconfig */; + baseConfigurationReference = B6357392C349F8B33892AE33 /* Pods-PlayKit-PlayKit_Tests.release.xcconfig */; buildSettings = { DEPLOYMENT_POSTPROCESSING = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 3.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PlayKit_Example.app/PlayKit_Example"; diff --git a/Example/PlayKit/Images.xcassets/AppIcon.appiconset/Contents.json b/Example/PlayKit/Images.xcassets/AppIcon.appiconset/Contents.json index d3942e94..b8236c65 100644 --- a/Example/PlayKit/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/Example/PlayKit/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "29x29", @@ -35,4 +45,4 @@ "version" : 1, "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Example/PlayKit/Info.plist b/Example/PlayKit/Info.plist index 3bf7d632..02954d25 100644 --- a/Example/PlayKit/Info.plist +++ b/Example/PlayKit/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion en + CFBundleDisplayName + PlayKit CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.0.8 + 0.1.x-dev CFBundleSignature ???? CFBundleVersion diff --git a/Example/Podfile b/Example/Podfile index dbc23c7a..8543a6c7 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -1,13 +1,16 @@ use_frameworks! +source 'https://github.com/kaltura/playkit-ios-widevine.git' +source 'https://github.com/CocoaPods/Specs.git' abstract_target 'PlayKit' do pod 'PlayKit', :path => '../../playkit-ios' + pod'GoogleAds-IMA-iOS-SDK', '~> 3.3' pod 'PlayKit/IMAPlugin', :path => '../../playkit-ios' pod 'PlayKit/YouboraPlugin', :path => '../../playkit-ios' + #pod 'PlayKit/WidevineClassic', :path => '../../playkit-ios' pod 'PlayKit/GoogleCastAddon', :path => '../../playkit-ios' - target 'PlayKit_Example' do end @@ -20,10 +23,16 @@ pre_install do |installer| def installer.verify_no_static_framework_transitive_dependencies; end end -post_install do |installer| - installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['SWIFT_VERSION'] = '3.0' config.build_settings['ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES'] = 'NO' + if target.name == "PlayKit.default-IMAPlugin" + config.build_settings['OTHER_LDFLAGS'] = '$(inherited) -framework "GoogleInteractiveMediaAds"' + config.build_settings['OTHER_SWIFT_FLAGS'] = '-DIMA_ENABLED' + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)', 'IMA_ENABLED=1'] + end end end end diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 4b3acb75..46fe22ef 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -2,29 +2,31 @@ PODS: - google-cast-sdk (3.3.0): - google-cast-sdk/Core (= 3.3.0) - google-cast-sdk/Core (3.3.0) - - GoogleAds-IMA-iOS-SDK (3.3.1) + - GoogleAds-IMA-iOS-SDK (3.4.1) - Log (1.0) - - PlayKit (0.0.8): - - PlayKit/Core (= 0.0.8) - - PlayKit/Core (0.0.8): + - PlayKit (0.1.x-dev): + - PlayKit/Core (= 0.1.x-dev) + - PlayKit/Core (0.1.x-dev): - Log - SwiftyJSON - - PlayKit/GoogleCastAddon (0.0.8): + - SwiftyXMLParser + - PlayKit/GoogleCastAddon (0.1.x-dev): - google-cast-sdk - - PlayKit/IMAPlugin (0.0.8): - - GoogleAds-IMA-iOS-SDK (~> 3.3) - - PlayKit/YouboraPlugin (0.0.8): - - Youbora-AVPlayer - - SwiftyJSON (3.1.3) - - Youbora-AVPlayer (5.3.5): - - Youbora-AVPlayer/default (= 5.3.5) - - Youbora-AVPlayer/default (5.3.5): - - Youbora-YouboraLib (= 5.3.8) - - Youbora-YouboraLib (5.3.8): - - Youbora-YouboraLib/default (= 5.3.8) - - Youbora-YouboraLib/default (5.3.8) + - PlayKit/Core + - PlayKit/IMAPlugin (0.1.x-dev): + - GoogleAds-IMA-iOS-SDK (= 3.4.1) + - PlayKit/Core + - PlayKit/YouboraPlugin (0.1.x-dev): + - PlayKit/Core + - Youbora-AVPlayer/dynamic + - SwiftyJSON (3.1.4) + - SwiftyXMLParser (3.0.0) + - Youbora-AVPlayer/dynamic (5.3.5): + - Youbora-YouboraLib/dynamic (= 5.3.8) + - Youbora-YouboraLib/dynamic (5.3.8) DEPENDENCIES: + - GoogleAds-IMA-iOS-SDK (~> 3.3) - PlayKit (from `../../playkit-ios`) - PlayKit/GoogleCastAddon (from `../../playkit-ios`) - PlayKit/IMAPlugin (from `../../playkit-ios`) @@ -36,13 +38,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: google-cast-sdk: da1989cbc1b9ff7b50ddb9dae5b1969d95a65a0f - GoogleAds-IMA-iOS-SDK: 5643300870bd3289c47c0d0b5a2dbf595b05c336 + GoogleAds-IMA-iOS-SDK: 7355db22ce69be4607ed0cc5112847b2b0c5e89d Log: 5e368c9528db07517d18d2d04ff5fe2b6f5a1e21 - PlayKit: 39cab83d21c70ec8f07241b2585a90c43df4d03b - SwiftyJSON: 38a8ea2006779c0fc4c310cb2ee8195327740faf + PlayKit: dc86a55ec81150014247d5b824215447d4ab4d0e + SwiftyJSON: c2842d878f95482ffceec5709abc3d05680c0220 + SwiftyXMLParser: 8d2295fb4fbc6e2ff241e7c8d7717e159be35969 Youbora-AVPlayer: 02aea2a12a4f7e6a61d8a1747e5dfc177bf2354b Youbora-YouboraLib: 523adf7cd09c4a213e3485e6ec7c48d498986246 -PODFILE CHECKSUM: a08b1d38cbc8e2181c5b19bebf0378f098e2f1ac +PODFILE CHECKSUM: 1fc877a43cbcd43ca40f1315bf706c4aa2f80f3b COCOAPODS: 1.2.0.beta.1 diff --git a/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift b/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift index 34764e37..d2ce1d5d 100644 --- a/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift +++ b/Example/Tests/MediaEntryProvider/MockMediaProviderTest.swift @@ -13,7 +13,6 @@ class MockMediaProviderTest: XCTestCase { override func setUp() { super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. let bundle = Bundle.main let path = bundle.path(forResource: "Entries", ofType: "json") @@ -29,17 +28,9 @@ class MockMediaProviderTest: XCTestCase { let json = JSON(data: data as Data) self.fileContent = json.object - - - - - - - } override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } diff --git a/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift b/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift index de0bd2e4..cd0a3c77 100644 --- a/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift +++ b/Example/Tests/MediaEntryProvider/OTTMediaProviderTest.swift @@ -26,11 +26,9 @@ class OTTMediaProviderTest: XCTestCase, SessionProvider { override func setUp() { super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } @@ -56,18 +54,7 @@ class OTTMediaProviderTest: XCTestCase, SessionProvider { self.waitForExpectations(timeout: 6.0) { (_) -> Void in } - - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - } diff --git a/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift b/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift index 1c0bc52f..e305e734 100644 --- a/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift +++ b/Example/Tests/MediaEntryProvider/OTTSessionProviderTest.swift @@ -13,19 +13,16 @@ class OTTSessionProviderTest: XCTestCase { override func setUp() { super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - - } + } override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testOTTSessionProvider() { let sessionProvider = OTTSessionManager(serverURL:"http://52.210.223.65:8080/v4_0/api_v3", partnerId:198, executor: nil) - sessionProvider.startAnonymouseSession { (e:Error?) in + sessionProvider.startAnonymousSession { (e:Error?) in if e == nil{ sessionProvider.loadKS(completion: { (r:Result) in print(r.data) @@ -37,12 +34,4 @@ class OTTSessionProviderTest: XCTestCase { } } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - } diff --git a/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift b/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift index c31a758c..2f0f0be2 100644 --- a/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift +++ b/Example/Tests/MediaEntryProvider/OVPMediaProviederTest.swift @@ -26,27 +26,19 @@ class OVPMediaProviederTest: XCTestCase, SessionProvider { override func setUp() { super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - - } override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testRegularCaseTest() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. let theExeption = expectation(description: "test") let provider = OVPMediaProvider() .set(sessionProvider: self) .set(entryId: self.entryID) .set(executor: MediaEntryProviderMockExecutor(entryID: entryID, domain: "ovp")) - - provider.loadMedia { (r:Result) in if (r.error != nil){ @@ -65,15 +57,13 @@ class OVPMediaProviederTest: XCTestCase, SessionProvider { func test_new_ovp_api() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. let theExeption = expectation(description: "test") let provider = OVPMediaProvider() .set(sessionProvider: self) .set(entryId: self.entryID) .set(executor: USRExecutor.shared ) - + provider.loadMedia { (r:Result) in if (r.error != nil){ @@ -89,15 +79,6 @@ class OVPMediaProviederTest: XCTestCase, SessionProvider { } } - - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - } diff --git a/Example/Tests/MessegeBusTest.swift b/Example/Tests/MessegeBusTest.swift index 177883b7..15bc6357 100644 --- a/Example/Tests/MessegeBusTest.swift +++ b/Example/Tests/MessegeBusTest.swift @@ -33,21 +33,19 @@ class MessegeBusTest: XCTestCase { let media = MediaEntry(json: entry) config.set(mediaEntry: media) self.player = PlayKitManager.sharedInstance.loadPlayer(config:config) - - + self.player.prepare(config) } override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testPlayerMetadataLoaded() { - let theExeption = expectation(description: "test loadedMetadata event") + let asyncExpectation = expectation(description: "test loadedMetadata event") - self.player.addObserver(self, events: [PlayerEvents.loadedMetadata.self]) { (info: Any) in - if info as! PKEvent is PlayerEvents.loadedMetadata { - theExeption.fulfill() + self.player.addObserver(self, events: [PlayerEvent.loadedMetadata]) { event in + if type(of: event) == PlayerEvent.loadedMetadata { + asyncExpectation.fulfill() } else { XCTFail() } @@ -59,19 +57,18 @@ class MessegeBusTest: XCTestCase { } func testPlayerPlayEventsFlow() { - let theExeption = expectation(description: "test play and playing + make sure playing is after play") + let asyncExpectation = expectation(description: "test play and playing + make sure playing is after play") var isPlay: Bool = false - self.player.addObserver(self, events: [PlayerEvents.play.self, PlayerEvents.playing.self]) { (info: Any) in - if info as! PKEvent is PlayerEvents.play { + self.player.addObserver(self, events: [PlayerEvent.play, PlayerEvent.playing]) { event in + if type(of: event) == PlayerEvent.play { isPlay = true - } else if info as! PKEvent is PlayerEvents.playing { + } else if type(of: event) == PlayerEvent.playing { if isPlay { - theExeption.fulfill() + asyncExpectation.fulfill() } else { XCTFail() } - } else { XCTFail() } @@ -85,10 +82,10 @@ class MessegeBusTest: XCTestCase { func testPlayerPauseEventsFlow() { let theExeption = expectation(description: "test pause") - self.player.addObserver(self, events: [PlayerEvents.playing.self, PlayerEvents.pause.self]) { (info: Any) in - if info as! PKEvent is PlayerEvents.playing { + self.player.addObserver(self, events: [PlayerEvent.playing, PlayerEvent.pause]) { event in + if type(of: event) == PlayerEvent.playing { self.player.pause() - } else if info as! PKEvent is PlayerEvents.pause { + } else if type(of: event) == PlayerEvent.pause { theExeption.fulfill() } else { XCTFail() @@ -101,22 +98,22 @@ class MessegeBusTest: XCTestCase { } func testPlayerSeekEventsFlow() { - let theExeption = expectation(description: "test seek") + let asyncExpectation = expectation(description: "test seek") let seekTime:TimeInterval = 3.0 var isSeeking = false - self.player.addObserver(self, events: [PlayerEvents.playing.self, PlayerEvents.seeking.self, PlayerEvents.seeked.self]) { (info: Any) in - if info as! PKEvent is PlayerEvents.playing { + self.player.addObserver(self, events: [PlayerEvent.playing, PlayerEvent.seeking, PlayerEvent.seeked]) { event in + if type(of: event) == PlayerEvent.playing { self.player.seek(to: CMTimeMakeWithSeconds(3, 1000000)) - } else if info as! PKEvent is PlayerEvents.seeking { + } else if type(of: event) == PlayerEvent.seeking { print(self.player.currentTime as Any) if (self.player.currentTime == seekTime){ isSeeking = true } else { XCTFail("seeking issue") } - } else if info as! PKEvent is PlayerEvents.seeked { + } else if type(of: event) == PlayerEvent.seeked { if isSeeking { - theExeption.fulfill() + asyncExpectation.fulfill() } else { XCTFail("seeking issue") } diff --git a/Example/Tests/PlayerControllerTest.swift b/Example/Tests/PlayerControllerTest.swift index 80b6b9c8..02b718c6 100644 --- a/Example/Tests/PlayerControllerTest.swift +++ b/Example/Tests/PlayerControllerTest.swift @@ -9,12 +9,13 @@ import XCTest import PlayKit import SwiftyJSON +import CoreMedia //Unit test to check the player controller //If somthing is break here we should raise a red flag - since this is our public API class PlayerControllerTest: XCTestCase { - var player : Player! + var player : Player! override func setUp() { super.setUp() @@ -35,41 +36,35 @@ class PlayerControllerTest: XCTestCase { let media = MediaEntry(json: entry) config.set(mediaEntry: media) self.player = PlayKitManager.sharedInstance.loadPlayer(config:config) - - -} + self.player.prepare(config) + } override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testPlayCommand() { - let theExeption = expectation(description: "play command") + let asyncExpectation = expectation(description: "play command") self.player.play(); - self.player.addObserver(self, events: [PlayerEvents.playing.self]) { (info: Any) in - if info is PlayerEvents.playing { - theExeption.fulfill() + self.player.addObserver(self, events: [PlayerEvent.playing]) { event in + if type(of: event) == PlayerEvent.playing { + asyncExpectation.fulfill() } else { XCTFail() } - - } waitForExpectations(timeout: 10.0) { (_) -> Void in} } func testPauseCommand() { - let theExeption = expectation(description: "pause command") + let asyncExpectation = expectation(description: "pause command") self.player.play(); - self.player.addObserver(self, events: [PlayerEvents.pause.self]) { (info: Any) in - if info is PlayerEvents.pause { - theExeption.fulfill() + self.player.addObserver(self, events: [PlayerEvent.pause]) { event in + if type(of: event) == PlayerEvent.pause { + asyncExpectation.fulfill() } else { XCTFail() } - - } self.player.pause() waitForExpectations(timeout: 10.0) { (_) -> Void in} @@ -77,34 +72,103 @@ class PlayerControllerTest: XCTestCase { func testIsPlayingValue() { - let theExeption = expectation(description: "play command") + let asyncExpectation = expectation(description: "play command") self.player.play(); - self.player.addObserver(self, events: [PlayerEvents.playing.self, PlayerEvents.pause.self]) { (info: Any) in + self.player.addObserver(self, events: [PlayerEvent.playing, PlayerEvent.pause]) { event in - if info is PlayerEvents.playing { + if type(of: event) == PlayerEvent.playing { if self.player.isPlaying { self.player.pause() } else { XCTFail() } - - } else if info is PlayerEvents.pause { + } else if type(of: event) == PlayerEvent.pause { if !self.player.isPlaying { - theExeption.fulfill() + asyncExpectation.fulfill() } else { XCTFail() } } } - waitForExpectations(timeout: 10.0) { (_) -> Void in} } - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. + /// Test a guard mechanism that prevents receiving pause events after ended event. + /// + /// ## The Flow: + /// 1. play the video. + /// 2. seek to 2 seconds before the end. + /// 3. on ended event pause the player. + /// + /// **expected result:** shouldn't receive the pause event and expectation should be fullfilled. + func testEnded() { + let asyncExpectation = expectation(description: "ended event") + var isEnded = false + var isFirstPlay = true + + self.player.addObserver(self, events: [PlayerEvent.ended]) { info in + print("ended") + isEnded = true + self.player.pause() + } + self.player.addObserver(self, events: [PlayerEvent.playing, PlayerEvent.pause]) { event in + if type(of: event) == PlayerEvent.playing && isFirstPlay && self.player.isPlaying { + isFirstPlay = false + // seek to end - 1 second + self.player.seek(to: CMTimeMake(Int64(self.player.duration - 2), 1)) + } + // should not fire play/pause after ended + if isEnded { + XCTFail() + } + } + player.play() + + DispatchQueue.main.asyncAfter(deadline: .now() + 9) { + asyncExpectation.fulfill() } + waitForExpectations(timeout: 10, handler: nil) } + /// Test to make sure pause/play events are received after ended + seeked event. + /// + /// ## The Flow: + /// 1. play the video. + /// 2. seek to 2 seconds before the end. + /// 3. on ended event seek again to 2 seconds before the end. + /// 4. play again + /// + /// **expected result:** receive play event after ended + seeked + func testAnalyticsEndedSeekedPlayed() { + let asyncExpectation = expectation(description: "ended -> seek -> play events") + var isEnded = false + var isFirstPlay = true + var isSeekedAfterEnded = false + + player.addObserver(self, events: [PlayerEvent.ended]) { info in + print("ended") + isEnded = true + self.player.seek(to: CMTimeMake(Int64(self.player.duration - 2), 1)) + } + player.addObserver(self, events: [PlayerEvent.playing, PlayerEvent.pause]) { event in + if type(of: event) == PlayerEvent.playing && isFirstPlay && self.player.isPlaying { + isFirstPlay = false + // seek to end - 2 second + self.player.seek(to: CMTimeMake(Int64(self.player.duration - 2), 1)) + } + // should fire play/pause after ended + seeked + if isSeekedAfterEnded { + asyncExpectation.fulfill() + } + } + player.addObserver(self, events: [PlayerEvent.seeked]) { info in + if isEnded { + isSeekedAfterEnded = true + self.player.play() + } + } + player.play() + + waitForExpectations(timeout: 20, handler: nil) + } } diff --git a/Example/Tests/SourceSelectorTest.swift b/Example/Tests/SourceSelectorTest.swift new file mode 100644 index 00000000..66c15988 --- /dev/null +++ b/Example/Tests/SourceSelectorTest.swift @@ -0,0 +1,63 @@ +// +// SourceSelectorTest.swift +// PlayKit +// +// Created by Noam Tamim on 12/01/2017. +// Copyright © 2017 CocoaPods. All rights reserved. +// + +import XCTest +import AVFoundation +@testable import PlayKit + +class SourceSelectorTest: XCTestCase { + + let mp4 = MediaSource("mp4", contentUrl: URL(string: "https://example.com/a.mp4"), sourceType: .mp4_clear) + let hls = MediaSource("hls", contentUrl: URL(string: "https://example.com/hls.m3u8"), sourceType: .hls_clear) + let fps = MediaSource("fps", contentUrl: URL(string: "https://example.com/fps.m3u8"), sourceType: .hls_fair_play) + let wvm = MediaSource("wvm", contentUrl: URL(string: "https://example.com/a.wvm"), sourceType: .wvm_wideVine) + + +// func entry(sources: [MediaSource]) -> MediaEntry { +// return MediaEntry.init("me", sources: <#T##[MediaSource]#>) +// } + + func testSelectedSource() { + + var builder: AssetBuilder + + builder = AssetBuilder(mediaEntry: MediaEntry("e", sources: [mp4, hls, fps])) + builder.build { (_, asset) in + guard let asset = asset as? AVURLAsset else { + XCTFail() + return + } + XCTAssertEqual(asset.url.lastPathComponent, "hls.m3u8") + } + + } + + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/PlayKit.podspec b/PlayKit.podspec index 45ab77f6..fb08dde8 100644 --- a/PlayKit.podspec +++ b/PlayKit.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |s| s.name = 'PlayKit' -s.version = '0.0.8' -s.summary = 'A short description of PlayKit.' +s.version = '0.1.x-dev' +s.summary = 'PlayKit: Kaltura Mobile Player SDK - iOS' s.homepage = 'https://github.com/kaltura/playkit-ios' -s.license = { :type => 'MIT', :file => 'LICENSE' } -s.author = { 'Rivka Schwartz' => 'Rivka.Peleg@kaltura.com', 'Vadim Kononov' => 'vadim.kononov@kaltura.com', 'Eliza Sapir' => 'eliza.sapir@kaltura.com', 'Noam Tamim' => 'noam.tamim@kaltura.com' } -s.source = { :git => 'https://github.com/kaltura/playkit-ios.git', :tag => s.version.to_s } +s.license = { :type => 'AGPLv3', :text => 'AGPLv3' } +s.author = { 'Kaltura' => 'community@kaltura.com' } +s.source = { :git => 'https://github.com/kaltura/playkit-ios.git', :tag => 'v' + s.version.to_s } s.ios.deployment_target = '8.0' @@ -15,10 +15,7 @@ s.subspec 'Core' do |sp| sp.source_files = 'Classes/**/*' sp.dependency 'SwiftyJSON' sp.dependency 'Log' -end - -s.subspec 'SamplePlugin' do |ssp| - ssp.source_files = 'Plugins/Sample' + sp.dependency 'SwiftyXMLParser' end s.subspec 'IMAPlugin' do |ssp| @@ -27,7 +24,8 @@ s.subspec 'IMAPlugin' do |ssp| 'OTHER_LDFLAGS' => '$(inherited) -framework "GoogleInteractiveMediaAds"', 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**', 'LIBRARY_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**' } - ssp.dependency 'GoogleAds-IMA-iOS-SDK', '~> 3.3' + ssp.dependency 'PlayKit/Core' + ssp.dependency 'GoogleAds-IMA-iOS-SDK', '3.4.1' end s.subspec 'GoogleCastAddon' do |ssp| @@ -37,6 +35,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 'PlayKit/Core' end s.subspec 'YouboraPlugin' do |ssp| @@ -45,13 +44,15 @@ s.subspec 'YouboraPlugin' do |ssp| 'OTHER_LDFLAGS' => '$(inherited) -framework "YouboraLib" -framework "YouboraPluginAVPlayer"', 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**', 'LIBRARY_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**' } - ssp.dependency 'Youbora-AVPlayer' + ssp.dependency 'Youbora-AVPlayer/dynamic' + ssp.dependency 'PlayKit/Core' end s.subspec 'WidevineClassic' do |ssp| ssp.source_files = 'Widevine' - ssp.dependency 'PlayKitWV' - ssp.pod_target_xcconfig = { 'ENABLE_BITCODE' => 'NO', 'GCC_PREPROCESSOR_DEFINITIONS'=>'WIDEVINE_ENABLED=1' } + ssp.dependency 'PlayKit/Core' + #ssp.dependency 'PlayKitWV' + #ssp.pod_target_xcconfig = { 'ENABLE_BITCODE' => 'NO', 'GCC_PREPROCESSOR_DEFINITIONS'=>'WIDEVINE_ENABLED=1' } end s.subspec 'PhoenixPlugin' do |ssp| @@ -60,6 +61,7 @@ s.subspec 'PhoenixPlugin' do |ssp| 'OTHER_LDFLAGS' => '$(inherited)', 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**', 'LIBRARY_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**' } + ssp.dependency 'PlayKit/Core' end s.subspec 'KalturaStatsPlugin' do |ssp| @@ -68,6 +70,7 @@ s.subspec 'KalturaStatsPlugin' do |ssp| 'OTHER_LDFLAGS' => '$(inherited)', 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**', ' LIBRARY_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**' } + ssp.dependency 'PlayKit/Core' end s.subspec 'KalturaLiveStatsPlugin' do |ssp| @@ -76,6 +79,7 @@ ssp.xcconfig = { 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES 'OTHER_LDFLAGS' => '$(inherited)', 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**', ' LIBRARY_SEARCH_PATHS' => '$(inherited) "${PODS_ROOT}"/**' } +ssp.dependency 'PlayKit/Core' end s.default_subspec = 'Core' diff --git a/PlayKitFramework/Podfile.lock b/PlayKitFramework/Podfile.lock index db0ce33a..3bd17f43 100644 --- a/PlayKitFramework/Podfile.lock +++ b/PlayKitFramework/Podfile.lock @@ -1,23 +1,21 @@ PODS: - - GoogleAds-IMA-iOS-SDK (3.3.1) - Log (1.0) - - PlayKit (0.1.0): - - PlayKit/Core (= 0.1.0) - - PlayKit/Core (0.1.0): + - PlayKit (0.1.7): + - PlayKit/Core (= 0.1.7) + - PlayKit/Core (0.1.7): - Log - SwiftyJSON - - PlayKit/IMAPlugin (0.1.0): - - GoogleAds-IMA-iOS-SDK (~> 3.3) - - PlayKit/YouboraPlugin (0.1.0): - - Youbora-AVPlayer - - SwiftyJSON (3.1.3) - - Youbora-AVPlayer (5.3.5): - - Youbora-AVPlayer/default (= 5.3.5) - - Youbora-AVPlayer/default (5.3.5): - - Youbora-YouboraLib (= 5.3.8) - - Youbora-YouboraLib (5.3.8): - - Youbora-YouboraLib/default (= 5.3.8) - - Youbora-YouboraLib/default (5.3.8) + - SwiftyXMLParser + - PlayKit/IMAPlugin (0.1.7): + - PlayKit/Core + - PlayKit/YouboraPlugin (0.1.7): + - PlayKit/Core + - Youbora-AVPlayer/dynamic + - SwiftyJSON (3.1.4) + - SwiftyXMLParser (3.0.0) + - Youbora-AVPlayer/dynamic (5.3.5): + - Youbora-YouboraLib/dynamic (= 5.3.8) + - Youbora-YouboraLib/dynamic (5.3.8) DEPENDENCIES: - PlayKit (from `../`) @@ -29,13 +27,13 @@ EXTERNAL SOURCES: :path: ../ SPEC CHECKSUMS: - GoogleAds-IMA-iOS-SDK: 5643300870bd3289c47c0d0b5a2dbf595b05c336 Log: 5e368c9528db07517d18d2d04ff5fe2b6f5a1e21 - PlayKit: 3b76cbb340c2b65792eb63e23e84b5e6e0a594ac - SwiftyJSON: 38a8ea2006779c0fc4c310cb2ee8195327740faf + PlayKit: a7f92b59a236c9c6fbe8b36d4e862dcf76001d5a + SwiftyJSON: c2842d878f95482ffceec5709abc3d05680c0220 + SwiftyXMLParser: 8d2295fb4fbc6e2ff241e7c8d7717e159be35969 Youbora-AVPlayer: 02aea2a12a4f7e6a61d8a1747e5dfc177bf2354b Youbora-YouboraLib: 523adf7cd09c4a213e3485e6ec7c48d498986246 PODFILE CHECKSUM: 88b52776b9ccf62b9480ac9262726c7b15947dc9 -COCOAPODS: 1.1.0 +COCOAPODS: 1.1.1 diff --git a/Plugins/IMA/IMAPlugin.swift b/Plugins/IMA/IMAPlugin.swift index 93a99acf..83815223 100644 --- a/Plugins/IMA/IMAPlugin.swift +++ b/Plugins/IMA/IMAPlugin.swift @@ -9,10 +9,11 @@ import GoogleInteractiveMediaAds public class IMAPlugin: NSObject, AVPictureInPictureControllerDelegate, PlayerDecoratorProvider, AdsPlugin, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMAWebOpenerDelegate, IMAContentPlayhead { - - private var player: Player! - private var messageBus: MessageBus? + public weak var mediaEntry: MediaEntry? + + private unowned var player: Player + private unowned var messageBus: MessageBus weak var dataSource: AdsPluginDataSource? { didSet { @@ -33,7 +34,7 @@ public class IMAPlugin: NSObject, AVPictureInPictureControllerDelegate, PlayerDe private var pictureInPictureProxy: IMAPictureInPictureProxy? private var loadingView: UIView? - private var config: AdsConfig! + private var config: AdsConfig? private var adTagUrl: String? private var tagsTimes: [TimeInterval : String]? { didSet { @@ -55,73 +56,72 @@ public class IMAPlugin: NSObject, AVPictureInPictureControllerDelegate, PlayerDe } } - override public required init() { - - } - - //MARK: plugin protocol methods - - public static var pluginName: String { - get { - return String(describing: IMAPlugin.self) - } - } + /************************************************************/ + // MARK: - PKPlugin + /************************************************************/ - public func load(player: Player, mediaConfig: MediaEntry, pluginConfig: Any?, messageBus: MessageBus) { - PKLog.trace("load") - + public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) { self.messageBus = messageBus - + self.player = player + super.init() if let adsConfig = pluginConfig as? AdsConfig { self.config = adsConfig - self.player = player - if IMAPlugin.loader == nil { - self.setupLoader(with: self.config) + self.setupLoader(with: adsConfig) } IMAPlugin.loader.contentComplete() IMAPlugin.loader.delegate = self - if let adTagUrl = self.config.adTagUrl { + if let adTagUrl = adsConfig.adTagUrl { self.adTagUrl = adTagUrl - } else if let adTagsTimes = self.config.tagsTimes { + } else if let adTagsTimes = adsConfig.tagsTimes { self.tagsTimes = adTagsTimes + self.sortedTagsTimes = adTagsTimes.keys.sorted() } } - + var events: [PKEvent.Type] = [] - events.append(PlayerEvents.ended.self) - self.messageBus?.addObserver(self, events: events, block: { (data: Any) -> Void in + events.append(PlayerEvent.ended) + self.messageBus.addObserver(self, events: events, block: { (data: Any) -> Void in self.contentComplete() }) - + self.timer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(IMAPlugin.update), userInfo: nil, repeats: true) } - + + public func onLoad(mediaConfig: MediaConfig) { + PKLog.trace("plugin \(type(of:self)) onLoad with media config: \(mediaConfig)") + self.mediaEntry = mediaConfig.mediaEntry + } + + public func onUpdateMedia(mediaConfig: MediaConfig) { + PKLog.trace("plugin \(type(of:self)) onUpdateMedia with media config: \(mediaConfig)") + self.mediaEntry = mediaConfig.mediaEntry + } + + public static var pluginName = String(describing: IMAPlugin.self) + public func destroy() { PKLog.trace("destroy") self.destroyManager() - self.player = nil self.timer?.invalidate() } + /************************************************************/ + // MARK: - Internal + /************************************************************/ + func getPlayerDecorator() -> PlayerDecoratorBase? { return AdsEnabledPlayerController(adsPlugin: self) } - //MARK: public methods func requestAds() { if self.adTagUrl != nil && self.adTagUrl != "" { self.startAdCalled = false var request: IMAAdsRequest - -// if let avPlayer = self.player.playerEngine as? AVPlayer { -// request = IMAAdsRequest(adTagUrl: self.adTagUrl, adDisplayContainer: self.createAdDisplayContainer(), avPlayerVideoDisplay: IMAAVPlayerVideoDisplay(avPlayer: avPlayer), pictureInPictureProxy: self.pictureInPictureProxy, userContext: nil) -// } else { - request = IMAAdsRequest(adTagUrl: self.adTagUrl, adDisplayContainer: self.createAdDisplayContainer(), contentPlayhead: self, userContext: nil) -// } + request = IMAAdsRequest(adTagUrl: self.adTagUrl, adDisplayContainer: self.createAdDisplayContainer(), contentPlayhead: self, userContext: nil) IMAPlugin.loader.requestAds(with: request) PKLog.trace("request Ads") @@ -161,7 +161,9 @@ public class IMAPlugin: NSObject, AVPictureInPictureControllerDelegate, PlayerDe IMAPlugin.loader.contentComplete() } - //MARK: private methods + /************************************************************/ + // MARK: - Private + /************************************************************/ private func setupLoader(with config: AdsConfig) { let imaSettings: IMASettings! = IMASettings() @@ -177,8 +179,8 @@ public class IMAPlugin: NSObject, AVPictureInPictureControllerDelegate, PlayerDe // self.pictureInPictureProxy = IMAPictureInPictureProxy(avPictureInPictureControllerDelegate: self) // } - if (self.config.companionView != nil) { - self.companionSlot = IMACompanionAdSlot(view: self.config.companionView, width: Int32(self.config.companionView!.frame.size.width), height: Int32(self.config.companionView!.frame.size.height)) + if let companionView = self.config?.companionView { + self.companionSlot = IMACompanionAdSlot(view: companionView, width: Int32(companionView.frame.size.width), height: Int32(companionView.frame.size.height)) } } @@ -206,7 +208,7 @@ public class IMAPlugin: NSObject, AVPictureInPictureControllerDelegate, PlayerDe } private func createAdDisplayContainer() -> IMAAdDisplayContainer { - return IMAAdDisplayContainer(adContainer: self.player.view, companionSlots: self.config.companionView != nil ? [self.companionSlot!] : nil) + return IMAAdDisplayContainer(adContainer: self.player.view, companionSlots: self.config?.companionView != nil ? [self.companionSlot!] : nil) } private func loadAdsIfNeeded() { @@ -246,21 +248,24 @@ public class IMAPlugin: NSObject, AVPictureInPictureControllerDelegate, PlayerDe @objc private func update() { if !self.isAdPlayback { - self.currentPlaybackTime = self.player.currentTime + let currentTime = self.player.currentTime + if currentTime.isNaN { + return + } + self.currentPlaybackTime = currentTime self.loadAdsIfNeeded() } } private func createRenderingSettings() { self.renderingSettings.webOpenerDelegate = self - if let webOpenerPresentingController = self.config.webOpenerPresentingController { + if let webOpenerPresentingController = self.config?.webOpenerPresentingController { self.renderingSettings.webOpenerPresentingController = webOpenerPresentingController } - - if let bitrate = self.config.videoBitrate { + if let bitrate = self.config?.videoBitrate { self.renderingSettings.bitrate = bitrate } - if let mimeTypes = self.config.videoMimeTypes { + if let mimeTypes = self.config?.videoMimeTypes { self.renderingSettings.mimeTypes = mimeTypes } } @@ -276,50 +281,50 @@ public class IMAPlugin: NSObject, AVPictureInPictureControllerDelegate, PlayerDe self.player.view?.bringSubview(toFront: self.loadingView!) } - private func convertToPlayerEvent(_ event: IMAAdEventType) -> AdEvents.Type { + private func convertToPlayerEvent(_ event: IMAAdEventType) -> AdEvent.Type { switch event { case .AD_BREAK_READY: - return AdEvents.adBreakReady.self + return AdEvent.adBreakReady case .AD_BREAK_ENDED: - return AdEvents.adBreakEnded.self + return AdEvent.adBreakEnded case .AD_BREAK_STARTED: - return AdEvents.adBreakStarted.self + return AdEvent.adBreakStarted case .ALL_ADS_COMPLETED: - return AdEvents.adAllCompleted.self + return AdEvent.adAllCompleted case .CLICKED: - return AdEvents.adClicked.self + return AdEvent.adClicked case .COMPLETE: - return AdEvents.adComplete.self + return AdEvent.adComplete case .CUEPOINTS_CHANGED: - return AdEvents.adCuepointsChanged.self + return AdEvent.adCuepointsChanged case .FIRST_QUARTILE: - return AdEvents.adFirstQuartile.self + return AdEvent.adFirstQuartile case .LOADED: - return AdEvents.adLoaded.self + return AdEvent.adLoaded case .LOG: - return AdEvents.adLog.self + return AdEvent.adLog case .MIDPOINT: - return AdEvents.adMidpoint.self + return AdEvent.adMidpoint case .PAUSE: - return AdEvents.adPaused.self + return AdEvent.adPaused case .RESUME: - return AdEvents.adResumed.self + return AdEvent.adResumed case .SKIPPED: - return AdEvents.adSkipped.self + return AdEvent.adSkipped case .STARTED: - return AdEvents.adStarted.self + return AdEvent.adStarted case .STREAM_LOADED: - return AdEvents.adStreamLoaded.self + return AdEvent.adStreamLoaded case .TAPPED: - return AdEvents.adTapped.self + return AdEvent.adTapped case .THIRD_QUARTILE: - return AdEvents.adThirdQuartile.self + return AdEvent.adThirdQuartile } } - private func notify(event: AdEvents) { + private func notify(event: AdEvent) { self.delegate?.adsPlugin(self, didReceive: event) - self.messageBus?.post(event) + self.messageBus.post(event) } private func destroyManager() { @@ -330,19 +335,17 @@ public class IMAPlugin: NSObject, AVPictureInPictureControllerDelegate, PlayerDe // MARK: AdsLoaderDelegate public func adsLoader(_ loader: IMAAdsLoader!, adsLoadedWith adsLoadedData: IMAAdsLoadedData!) { - if let _ = self.player { - self.loaderFailed = false - - self.manager = adsLoadedData.adsManager - self.manager!.delegate = self - self.createRenderingSettings() - - if self.startAdCalled { - self.manager!.initialize(with: self.renderingSettings) - } - - PKLog.trace("ads manager set") + self.loaderFailed = false + + self.manager = adsLoadedData.adsManager + self.manager!.delegate = self + self.createRenderingSettings() + + if self.startAdCalled { + self.manager!.initialize(with: self.renderingSettings) } + + PKLog.trace("ads manager set") } public func adsLoader(_ loader: IMAAdsLoader!, failedWith adErrorData: IMAAdLoadingErrorData!) { @@ -402,13 +405,13 @@ public class IMAPlugin: NSObject, AVPictureInPictureControllerDelegate, PlayerDe } public func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager!) { - self.notify(event: AdEvents.adDidRequestPause()) + self.notify(event: AdEvent.AdDidRequestPause()) self.isAdPlayback = true } public func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager!) { self.showLoadingView(false, alpha: 0) - self.notify(event: AdEvents.adDidRequestResume()) + self.notify(event: AdEvent.AdDidRequestResume()) self.isAdPlayback = false } @@ -416,7 +419,7 @@ public class IMAPlugin: NSObject, AVPictureInPictureControllerDelegate, PlayerDe var data = [String : TimeInterval]() data["mediaTime"] = mediaTime data["totalTime"] = totalTime - self.notify(event: AdEvents.adDidProgressToTime(mediaTime: mediaTime, totalTime: totalTime)) + self.notify(event: AdEvent.AdDidProgressToTime(mediaTime: mediaTime, totalTime: totalTime)) } // MARK: AVPictureInPictureControllerDelegate @@ -454,22 +457,22 @@ public class IMAPlugin: NSObject, AVPictureInPictureControllerDelegate, PlayerDe // MARK: IMAWebOpenerDelegate public func webOpenerWillOpenExternalBrowser(_ webOpener: NSObject) { - self.notify(event: AdEvents.adWebOpenerWillOpenExternalBrowser(webOpener: webOpener)) + self.notify(event: AdEvent.AdWebOpenerWillOpenExternalBrowser(webOpener: webOpener)) } public func webOpenerWillOpen(inAppBrowser webOpener: NSObject!) { - self.notify(event: AdEvents.adWebOpenerWillOpenInAppBrowser(webOpener: webOpener)) + self.notify(event: AdEvent.AdWebOpenerWillOpenInAppBrowser(webOpener: webOpener)) } public func webOpenerDidOpen(inAppBrowser webOpener: NSObject!) { - self.notify(event: AdEvents.adWebOpenerDidOpenInAppBrowser(webOpener: webOpener)) + self.notify(event: AdEvent.AdWebOpenerDidOpenInAppBrowser(webOpener: webOpener)) } public func webOpenerWillClose(inAppBrowser webOpener: NSObject!) { - self.notify(event: AdEvents.adWebOpenerWillCloseInAppBrowser(webOpener: webOpener)) + self.notify(event: AdEvent.AdWebOpenerWillCloseInAppBrowser(webOpener: webOpener)) } public func webOpenerDidClose(inAppBrowser webOpener: NSObject!) { - self.notify(event: AdEvents.adWebOpenerDidCloseInAppBrowser(webOpener: webOpener)) + self.notify(event: AdEvent.AdWebOpenerDidCloseInAppBrowser(webOpener: webOpener)) } } diff --git a/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift b/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift index 9f65f5d4..1640effb 100644 --- a/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift +++ b/Plugins/KalturaLiveStats/KalturaLiveStatsPlugin.swift @@ -13,10 +13,9 @@ public class KalturaLiveStatsPlugin: PKPlugin { case DVR = 2 } - private var player: Player! - private var messageBus: MessageBus? - private var config: AnalyticsConfig! - private var mediaEntry: MediaEntry! + private unowned var player: Player + private unowned var messageBus: MessageBus + private var config: AnalyticsConfig? public static var pluginName: String = "KalturaLiveStats" @@ -34,22 +33,29 @@ public class KalturaLiveStatsPlugin: PKPlugin { private var timer: Timer? private var interval = 10 - required public init() { - - } + /************************************************************/ + // MARK: - PKPlugin + /************************************************************/ - public func load(player: Player, mediaConfig: MediaEntry, pluginConfig: Any?, messageBus: MessageBus) { - + public weak var mediaEntry: MediaEntry? + + public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) { + self.player = player self.messageBus = messageBus - self.mediaEntry = mediaConfig - if let aConfig = pluginConfig as? AnalyticsConfig { self.config = aConfig - self.player = player } - - registerToAllEvents() - + self.registerToAllEvents() + } + + public func onLoad(mediaConfig: MediaConfig) { + PKLog.trace("plugin \(type(of:self)) onLoad with media config: \(mediaConfig)") + self.mediaEntry = mediaConfig.mediaEntry + } + + public func onUpdateMedia(mediaConfig: MediaConfig) { + PKLog.trace("plugin \(type(of:self)) onUpdateMedia with media config: \(mediaConfig)") + self.mediaEntry = mediaConfig.mediaEntry } public func destroy() { @@ -59,51 +65,50 @@ public class KalturaLiveStatsPlugin: PKPlugin { } } + /************************************************************/ + // MARK: - Private + /************************************************************/ + private func registerToAllEvents() { PKLog.trace("registerToAllEvents") - self.messageBus?.addObserver(self, events: [PlayerEvents.play.self], block: { (info) in - PKLog.trace("play info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.play], block: { (event) in + PKLog.trace("play info: \(event)") self.lastReportedStartTime = self.player.currentTime.toInt32() self.startLiveEvents() }) - self.messageBus?.addObserver(self, events: [PlayerEvents.pause.self], block: { (info) in - PKLog.trace("pause info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.pause], block: { (event) in + PKLog.trace("pause info: \(event)") self.stopLiveEvents() }) - self.messageBus?.addObserver(self, events: [PlayerEvents.playbackParamsUpdated.self], block: { (info) in - PKLog.trace("playbackParamsUpdated info: \(info)") - if let paramsEvent = info as? PlayerEvents.playbackParamsUpdated { - self.lastReportedBitrate = Int32(paramsEvent.currentBitrate) + self.messageBus.addObserver(self, events: [PlayerEvent.playbackParamsUpdated], block: { event in + PKLog.trace("playbackParamsUpdated info: \(event)") + if type(of: event) == PlayerEvent.playbackParamsUpdated { + self.lastReportedBitrate = Int32(event.currentBitrate!) } }) - self.player.addObserver(self, events: [PlayerEvents.stateChanged.self]) { (data: Any) in + self.messageBus.addObserver(self, events: [PlayerEvent.stateChanged]) { event in + PKLog.trace("playbackParamsUpdated info: \(event)") - if let stateChanged = data as? PlayerEvents.stateChanged { - - switch stateChanged.newSate { + if type(of: event) == PlayerEvent.stateChanged { + switch event.newState { case .ready: self.startTimer() if self.isBuffering { self.isBuffering = false self.sendLiveEvent(theBufferTime: self.calculateBuffer(isBuffering: false)) } - break case .buffering: self.isBuffering = true self.bufferStartTime = Date().timeIntervalSince1970.toInt32() - break - default: - - break + default: break } } } - } private func startLiveEvents() { @@ -123,7 +128,7 @@ public class KalturaLiveStatsPlugin: PKPlugin { private func createTimer() { - if let intr = self.config.params["timerInterval"] as? Int { + if let intr = self.config?.params["timerInterval"] as? Int { self.interval = intr } @@ -172,22 +177,23 @@ public class KalturaLiveStatsPlugin: PKPlugin { } private func sendLiveEvent(theBufferTime: Int32) { - PKLog.trace("sendLiveEvent - Buffer Time: \(bufferTime)") + guard let mediaEntry = self.mediaEntry else { return } + var sessionId = "" var baseUrl = "https://stats.kaltura.com/api_v3/index.php" var parterId = "" - if let sId = self.config.params["sessionId"] as? String { + if let sId = self.config?.params["sessionId"] as? String { sessionId = sId } - if let url = self.config.params["baseUrl"] as? String { + if let url = self.config?.params["baseUrl"] as? String { baseUrl = url } - if let pId = self.config.params["partnerId"] as? Int { + if let pId = self.config?.params["partnerId"] as? Int { parterId = String(pId) } @@ -199,7 +205,7 @@ public class KalturaLiveStatsPlugin: PKPlugin { bitrate: self.lastReportedBitrate, sessionId: sessionId, startTime: self.lastReportedStartTime, - entryId: self.mediaEntry.id, + entryId: mediaEntry.id, isLive: isLive, clientVer: PlayKitManager.clientTag, deliveryType: "hls") { @@ -209,9 +215,7 @@ public class KalturaLiveStatsPlugin: PKPlugin { PKLog.trace("Response: \(response)") } - USRExecutor.shared.send(request: builder.build()) - } } diff --git a/Plugins/KalturaStats/KalturaStatsPlugin.swift b/Plugins/KalturaStats/KalturaStatsPlugin.swift index 57692ca8..7a27c0d3 100644 --- a/Plugins/KalturaStats/KalturaStatsPlugin.swift +++ b/Plugins/KalturaStats/KalturaStatsPlugin.swift @@ -52,12 +52,12 @@ public class KalturaStatsPlugin: PKPlugin { case ERROR = 99 } - private var player: Player! - private var messageBus: MessageBus? - private var config: AnalyticsConfig! - private var mediaEntry: MediaEntry! + private unowned var player: Player + private unowned var messageBus: MessageBus + private var config: AnalyticsConfig? public static var pluginName: String = "KalturaStatsPlugin" + public weak var mediaEntry: MediaEntry? private var isFirstPlay = true private var isWidgetLoaded = false @@ -76,22 +76,27 @@ public class KalturaStatsPlugin: PKPlugin { private var timer: Timer? private var interval = 30 - required public init() { - - } + /************************************************************/ + // MARK: - PKPlugin + /************************************************************/ - public func load(player: Player, mediaConfig: MediaEntry, pluginConfig: Any?, messageBus: MessageBus) { - + public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) { + self.player = player self.messageBus = messageBus - self.mediaEntry = mediaConfig - if let aConfig = pluginConfig as? AnalyticsConfig { self.config = aConfig - self.player = player } - - registerToAllEvents() - + self.registerToAllEvents() + } + + public func onLoad(mediaConfig: MediaConfig) { + PKLog.trace("plugin \(type(of:self)) onLoad with media config: \(mediaConfig)") + self.mediaEntry = mediaConfig.mediaEntry + } + + public func onUpdateMedia(mediaConfig: MediaConfig) { + PKLog.trace("plugin \(type(of:self)) onUpdateMedia with media config: \(mediaConfig)") + self.mediaEntry = mediaConfig.mediaEntry } public func destroy() { @@ -100,53 +105,54 @@ public class KalturaStatsPlugin: PKPlugin { } } + /************************************************************/ + // MARK: - Private Implementation + /************************************************************/ + private func registerToAllEvents() { - PKLog.trace("registerToAllEvents") - self.messageBus?.addObserver(self, events: [PlayerEvents.canPlay.self], block: { (info) in - PKLog.trace("canPlay info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.canPlay], block: { [unowned self] (event) in + PKLog.trace("canPlay event: \(event)") self.sendMediaLoaded() }) - self.messageBus?.addObserver(self, events: [PlayerEvents.play.self, PlayerEvents.playing.self], block: { (info) in - PKLog.trace("play info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.play, PlayerEvent.playing], block: { [unowned self] (event) in + PKLog.trace("play event: \(event)") if self.isFirstPlay { self.sendAnalyticsEvent(action: .PLAY) self.isFirstPlay = false } }) - self.messageBus?.addObserver(self, events: [PlayerEvents.pause.self], block: { (info) in - PKLog.trace("pause info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.pause], block: { [unowned self] (event) in + PKLog.trace("pause event: \(event)") }) - self.messageBus?.addObserver(self, events: [PlayerEvents.seeking.self], block: { (info) in - PKLog.trace("seeking info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.seeking], block: { [unowned self] (event) in + PKLog.trace("seeking event: \(event)") }) - self.messageBus?.addObserver(self, events: [PlayerEvents.seeked.self], block: { (info) in - PKLog.trace("seeked info: \(info)") - + self.messageBus.addObserver(self, events: [PlayerEvent.seeked], block: { [unowned self] (event) in + PKLog.trace("seeked event: \(event)") self.hasSeeked = true self.seekPercent = Float(self.player.currentTime) / Float(self.player.duration) self.sendAnalyticsEvent(action: .SEEK); }) - self.messageBus?.addObserver(self, events: [PlayerEvents.ended.self], block: { (info) in - PKLog.trace("ended info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.ended], block: { [unowned self] (event) in + PKLog.trace("ended event: \(event)") }) - self.messageBus?.addObserver(self, events: [PlayerEvents.error.self], block: { (info) in - PKLog.trace("error info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.error], block: { [unowned self] (event) in + PKLog.trace("error event: \(event)") self.sendAnalyticsEvent(action: .ERROR) }) - self.player.addObserver(self, events: [PlayerEvents.stateChanged.self]) { (data: Any) in - - if let stateChanged = data as? PlayerEvents.stateChanged { - - switch stateChanged.newSate { + self.messageBus.addObserver(self, events: [PlayerEvent.stateChanged]) { [unowned self] event in + PKLog.trace("state changed event: \(event)") + if let stateChanged = event as? PlayerEvent.StateChanged { + switch stateChanged.newState { case .idle: self.sendWidgetLoaded() break @@ -167,20 +173,17 @@ public class KalturaStatsPlugin: PKPlugin { self.createTimer() } self.sendMediaLoaded() - break case .buffering: self.sendWidgetLoaded() self.isBuffering = true self.sendAnalyticsEvent(action: .BUFFER_START) break - case .error: - - break + case .error: break + case .unknown: break } } } - } private func sendWidgetLoaded() { @@ -212,7 +215,7 @@ public class KalturaStatsPlugin: PKPlugin { private func createTimer() { - if let intr = self.config.params["timerInterval"] as? Int { + if let intr = self.config?.params["timerInterval"] as? Int { self.interval = intr } @@ -270,22 +273,27 @@ public class KalturaStatsPlugin: PKPlugin { var confId = 0 var parterId = "" - if let sId = self.config.params["sessionId"] as? String { + if let sId = self.config?.params["sessionId"] as? String { sessionId = sId } - if let cId = self.config.params["uiconfId"] as? Int { + if let cId = self.config?.params["uiconfId"] as? Int { confId = cId } - if let url = self.config.params["baseUrl"] as? String { + if let url = self.config?.params["baseUrl"] as? String { baseUrl = url } - if let pId = self.config.params["partnerId"] as? Int { + if let pId = self.config?.params["partnerId"] as? Int { parterId = String(pId) } + guard let mediaEntry = self.mediaEntry else { + PKLog.error("send analytics failed due to nil mediaEntry") + return + } + let builder: KalturaRequestBuilder = OVPStatsService.get(baseURL: baseUrl, partnerId: parterId, eventType: action.rawValue, @@ -294,7 +302,7 @@ public class KalturaStatsPlugin: PKPlugin { sessionId: sessionId, position: self.player.currentTime.toInt32(), uiConfId: confId, - entryId: self.mediaEntry.id, + entryId: mediaEntry.id, widgetId: "_\(parterId)", isSeek: hasSeeked)! diff --git a/Plugins/Phoenix/BookmarkService.swift b/Plugins/Phoenix/BookmarkService.swift index f200e20d..7f90364c 100644 --- a/Plugins/Phoenix/BookmarkService.swift +++ b/Plugins/Phoenix/BookmarkService.swift @@ -48,7 +48,7 @@ internal class BookmarkService { private static func createBookmark(eventType: String, position: Int32, assetId: String, fileId: String) -> JSON { var json: JSON = JSON.init(["objectType": "KalturaBookmark"]) json["type"] = JSON("media") - //json["id"] = JSON(assetId) + json["id"] = JSON(assetId) json["position"] = JSON(position) json["playerData"] = JSON.init(["action": JSON(eventType), "objectType": JSON("KalturaBookmarkPlayerData"), "fileId": JSON(fileId)]) diff --git a/Plugins/Phoenix/KalturaPluginManager.swift b/Plugins/Phoenix/KalturaPluginManager.swift index bf4920e9..b06b71cf 100644 --- a/Plugins/Phoenix/KalturaPluginManager.swift +++ b/Plugins/Phoenix/KalturaPluginManager.swift @@ -8,7 +8,7 @@ import UIKit -internal enum PhoenixAnalyticsType: String { +enum PhoenixAnalyticsType: String { case hit case play case stop @@ -25,12 +25,12 @@ protocol KalturaPluginManagerDelegate { func pluginManagerDidSendAnalyticsEvent(action: PhoenixAnalyticsType) } -internal class KalturaPluginManager { +final class KalturaPluginManager { public var delegate: KalturaPluginManagerDelegate? - private var player: Player? - private var messageBus: MessageBus? + private unowned var player: Player + private unowned var messageBus: MessageBus private var config: AnalyticsConfig? private var isFirstPlay = true @@ -39,16 +39,18 @@ internal class KalturaPluginManager { private var timer: Timer? private var interval = 30 //Should be provided in plugin config - public func load(player: Player, pluginConfig: Any?, messageBus: MessageBus) { + init(player: Player, pluginConfig: Any?, messageBus: MessageBus) { + self.player = player self.messageBus = messageBus - + self.load(pluginConfig: pluginConfig) + } + + func load(pluginConfig: Any?) { if let aConfig = pluginConfig as? AnalyticsConfig { self.config = aConfig - self.player = player } - - registerToAllEvents() - + self.registerToAllEvents() + AppStateSubject.sharedInstance.add(observer: self) } public func destroy() { @@ -58,30 +60,37 @@ internal class KalturaPluginManager { } func registerToAllEvents() { - - - self.messageBus?.addObserver(self, events: [PlayerEvents.ended.self], block: { (info) in + PKLog.trace("Register to all events") + + self.messageBus.addObserver(self, events: [PlayerEvent.ended], block: { (info) in PKLog.trace("ended info: \(info)") self.stopTimer() self.delegate?.pluginManagerDidSendAnalyticsEvent(action: .finish) }) - self.messageBus?.addObserver(self, events: [PlayerEvents.error.self], block: { (info) in + self.messageBus.addObserver(self, events: [PlayerEvent.error], block: { (info) in PKLog.trace("error info: \(info)") self.delegate?.pluginManagerDidSendAnalyticsEvent(action: .error) }) - self.messageBus?.addObserver(self, events: [PlayerEvents.pause.self], block: { (info) in + self.messageBus.addObserver(self, events: [PlayerEvent.pause], block: { (info) in PKLog.trace("pause info: \(info)") + // 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 !self.isFirstPlay { + self.stopTimer() + self.intervalOn = false + } + self.delegate?.pluginManagerDidSendAnalyticsEvent(action: .pause) }) - self.messageBus?.addObserver(self, events: [PlayerEvents.loadedMetadata.self], block: { (info) in + self.messageBus.addObserver(self, events: [PlayerEvent.loadedMetadata], block: { (info) in PKLog.trace("loadedMetadata info: \(info)") self.delegate?.pluginManagerDidSendAnalyticsEvent(action: .load) }) - self.messageBus?.addObserver(self, events: [PlayerEvents.loadedMetadata.self], block: { (info) in + self.messageBus.addObserver(self, events: [PlayerEvent.playing], block: { (info) in PKLog.trace("play info: \(info)") if !self.intervalOn { @@ -95,12 +104,17 @@ internal class KalturaPluginManager { } else { self.delegate?.pluginManagerDidSendAnalyticsEvent(action: .play); } - - }) - } + public func reportConcurrencyEvent() { + self.messageBus.post(OttEvent.Concurrency()) + } + + /************************************************************/ + // MARK: - Private Implementation + /************************************************************/ + private func createTimer() { if let conf = self.config, let intr = conf.params["timerInterval"] as? Int { @@ -111,23 +125,26 @@ internal class KalturaPluginManager { t.invalidate() } + // media hit should fire on every time we start the timer. + self.sendProgressEvent() + self.timer = Timer.scheduledTimer(timeInterval: TimeInterval(self.interval), target: self, selector: #selector(KalturaPluginManager.timerHit), userInfo: nil, repeats: true) } @objc private func timerHit() { - PKLog.trace("timerHit") - + self.sendProgressEvent() + } + + private func sendProgressEvent() { self.delegate?.pluginManagerDidSendAnalyticsEvent(action: .hit); - if let player = self.player { - var progress = Float(player.currentTime) / Float(player.duration) - PKLog.trace("Progress is \(progress)") - - if progress > 0.98 { - self.delegate?.pluginManagerDidSendAnalyticsEvent(action: .finish) - } + var progress = Float(player.currentTime) / Float(player.duration) + PKLog.trace("Progress is \(progress)") + + if progress > 0.98 { + self.delegate?.pluginManagerDidSendAnalyticsEvent(action: .finish) } } @@ -136,8 +153,22 @@ internal class KalturaPluginManager { t.invalidate() } } +} + +/************************************************************/ +// MARK: - App State Handling +/************************************************************/ + +extension KalturaPluginManager: AppStateObservable { - public func reportConcurrencyEvent() { - self.messageBus?.post(OttEvent.OttEventConcurrency()) + var observations: Set { + return [ + NotificationObservation(name: .UIApplicationWillTerminate) { [unowned self] in + guard let delegate = self.delegate else { return } + PKLog.trace("plugin: \(delegate) will terminate event received, sending analytics stop event") + self.destroy() + AppStateSubject.sharedInstance.remove(observer: self) + } + ] } } diff --git a/Plugins/Phoenix/MediaMarkService.swift b/Plugins/Phoenix/MediaMarkService.swift index ca7fdd4a..4ac04ffa 100644 --- a/Plugins/Phoenix/MediaMarkService.swift +++ b/Plugins/Phoenix/MediaMarkService.swift @@ -12,7 +12,7 @@ import SwiftyJSON internal class MediaMarkService { internal static func sendTVPAPIEVent(baseURL: String, - initObj: JSON?, + initObj: [String : Any], eventType: String, currentTime: Int32, assetId: String, @@ -21,11 +21,7 @@ internal class MediaMarkService { if let request: RequestBuilder = RequestBuilder(url: baseURL) { request .set(method: "POST") - - if let obj = initObj { - request.set(jsonBody: obj) - } - request + .setBody(key: "initObj", value: JSON(initObj)) .setBody(key: "iFileID", value: JSON(fileId)) .setBody(key: "iMediaID", value: JSON(assetId)) .setBody(key: "iLocation", value: JSON(currentTime)) diff --git a/Plugins/Phoenix/OttEvent.swift b/Plugins/Phoenix/OttEvent.swift index 5c46c0d1..f041ec17 100644 --- a/Plugins/Phoenix/OttEvent.swift +++ b/Plugins/Phoenix/OttEvent.swift @@ -8,8 +8,11 @@ import UIKit +/// OTT Event public class OttEvent : PKEvent { - - public class OttEventConcurrency : OttEvent {} - + + class Concurrency : OttEvent {} + /// represents the Concurrency event Type. + /// Concurrency events fire when more then the allowed connections are exceeded. + public static let concurrency: OttEvent.Type = Concurrency.self } diff --git a/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift b/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift index c75d61d1..f38b2be9 100644 --- a/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift +++ b/Plugins/Phoenix/PhoenixAnalyticsPlugin.swift @@ -10,37 +10,45 @@ import UIKit public class PhoenixAnalyticsPlugin: PKPlugin, KalturaPluginManagerDelegate { - public static var pluginName: String = "PhoenixAnalytics" - - private var player: Player! + private unowned var player: Player private var config: AnalyticsConfig! - private var mediaEntry: MediaEntry! - private var kalturaPluginManager: KalturaPluginManager! - required public init() { - - } + public static var pluginName: String = "PhoenixAnalytics" + public weak var mediaEntry: MediaEntry? - public func load(player: Player, mediaConfig: MediaEntry, pluginConfig: Any?, messageBus: MessageBus) { - self.kalturaPluginManager = KalturaPluginManager() - - self.mediaEntry = mediaConfig + /************************************************************/ + // MARK: - PKPlugin + /************************************************************/ + + public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) { + self.player = player if let aConfig = pluginConfig as? AnalyticsConfig { - self.player = player self.config = aConfig } - + self.kalturaPluginManager = KalturaPluginManager(player: player, pluginConfig: pluginConfig, messageBus: messageBus) self.kalturaPluginManager.delegate = self - self.kalturaPluginManager.load(player: player, pluginConfig: pluginConfig, messageBus: messageBus) - + } + + public func onLoad(mediaConfig: MediaConfig) { + PKLog.trace("plugin \(type(of:self)) onLoad with media config: \(mediaConfig)") + self.mediaEntry = mediaConfig.mediaEntry + } + + public func onUpdateMedia(mediaConfig: MediaConfig) { + PKLog.trace("plugin \(type(of:self)) onUpdateMedia with media config: \(mediaConfig)") + self.mediaEntry = mediaConfig.mediaEntry } public func destroy() { self.kalturaPluginManager.destroy() } - internal func pluginManagerDidSendAnalyticsEvent(action: PhoenixAnalyticsType) { + /************************************************************/ + // MARK: - KalturaPluginManagerDelegate + /************************************************************/ + + func pluginManagerDidSendAnalyticsEvent(action: PhoenixAnalyticsType) { PKLog.trace("Action: \(action)") var fileId = "" @@ -64,37 +72,33 @@ public class PhoenixAnalyticsPlugin: PKPlugin, KalturaPluginManagerDelegate { parterId = pId } + guard let mediaEntry = self.mediaEntry else { + PKLog.error("send analytics failed due to nil mediaEntry") + return + } + if let builder: KalturaRequestBuilder = BookmarkService.actionAdd(baseURL: baseUrl, - partnerId: parterId, - ks: ks, - eventType: action.rawValue.uppercased(), - currentTime: self.player.currentTime.toInt32(), - assetId: self.mediaEntry.id, - fileId: fileId) { + partnerId: parterId, + ks: ks, + eventType: action.rawValue.uppercased(), + currentTime: self.player.currentTime.toInt32(), + assetId: mediaEntry.id, + fileId: fileId) { builder.set { (response: Response) in - PKLog.trace("Response: \(response)") if response.statusCode == 0 { PKLog.trace("\(response.data)") - if let data : [String: Any] = response.data as! [String : Any]? { - if let result = data["result"] as! [String: Any]? { - if let errorData = result["error"] as! [String: Any]? { - if let errorCode = errorData["code"] as? Int, errorCode == 4001 { - - self.kalturaPluginManager.reportConcurrencyEvent() - } - } - } - } + PKLog.trace("\(response.data)") + guard let data = response.data as? [String : Any] else { return } + guard let result = data["result"] as? [String: Any] else { return } + guard let errorData = result["error"] as? [String: Any] else { return } + guard let errorCode = errorData["code"] as? Int, errorCode == 4001 else { return } + self.kalturaPluginManager.reportConcurrencyEvent() } - } - USRExecutor.shared.send(request: builder.build()) } - } - } diff --git a/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift b/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift index 97908b1e..799663c4 100644 --- a/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift +++ b/Plugins/Phoenix/TVPAPIAnalyticsPlugin.swift @@ -10,83 +10,87 @@ import UIKit import SwiftyJSON public class TVPAPIAnalyticsPlugin: PKPlugin, KalturaPluginManagerDelegate { - - public static var pluginName: String = "TVPAPIAnalytics" - private var player: Player! - private var config: AnalyticsConfig! - private var mediaEntry: MediaEntry! + public static var pluginName: String = "TVPAPIAnalytics" + public weak var mediaEntry: MediaEntry? - private var kalturaPluginManager: KalturaPluginManager! + private unowned var player: Player + private var config: AnalyticsConfig? + private var kalturaPluginManager: KalturaPluginManager - required public init() { - - } + /************************************************************/ + // MARK: - PKPlugin + /************************************************************/ - public func load(player: Player, mediaConfig: MediaEntry, pluginConfig: Any?, messageBus: MessageBus) { - self.kalturaPluginManager = KalturaPluginManager() - - self.mediaEntry = mediaConfig + public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) { + self.player = player if let aConfig = pluginConfig as? AnalyticsConfig { - self.player = player self.config = aConfig } - + self.kalturaPluginManager = KalturaPluginManager(player: player, pluginConfig: pluginConfig, messageBus: messageBus) self.kalturaPluginManager.delegate = self - self.kalturaPluginManager.load(player: player, pluginConfig: pluginConfig, messageBus: messageBus) - + } + + public func onLoad(mediaConfig: MediaConfig) { + PKLog.trace("plugin \(type(of:self)) onLoad with media config: \(mediaConfig)") + self.mediaEntry = mediaConfig.mediaEntry + } + + public func onUpdateMedia(mediaConfig: MediaConfig) { + PKLog.trace("plugin \(type(of:self)) onUpdateMedia with media config: \(mediaConfig)") + self.mediaEntry = mediaConfig.mediaEntry } public func destroy() { self.kalturaPluginManager.destroy() } + /************************************************************/ + // MARK: - KalturaPluginManagerDelegate + /************************************************************/ + internal func pluginManagerDidSendAnalyticsEvent(action: PhoenixAnalyticsType) { PKLog.trace("Action: \(action)") var fileId = "" var baseUrl = "" - var initObj : JSON? = nil + + guard let initObj = self.config?.params["initObj"] as? [String : Any] else { + PKLog.error("send analytics failed due to no initObj data") + return + } + + guard let mediaEntry = self.mediaEntry else { + PKLog.error("send analytics failed due to nil mediaEntry") + return + } let method = action == .hit ? "MediaHit" : "MediaMark" - - if let url = self.config.params["baseUrl"] as? String { + + if let url = self.config?.params["baseUrl"] as? String { baseUrl = url } - - if let fId = self.config.params["fileId"] as? String { + if let fId = self.config?.params["fileId"] as? String { fileId = fId } - if let obj = self.config.params["initObj"] as? JSON { - initObj = obj - } - baseUrl = "\(baseUrl)m=\(method)" if let builder: RequestBuilder = MediaMarkService.sendTVPAPIEVent(baseURL: baseUrl, initObj: initObj, eventType: action.rawValue, currentTime: self.player.currentTime.toInt32(), - assetId: self.mediaEntry.id, + assetId: mediaEntry.id, fileId: fileId) { builder.set { (response: Response) in - PKLog.trace("Response: \(response)") if response.statusCode == 0 { - PKLog.trace("\(response.data)") - if let data : [String: Any] = response.data as! [String : Any]? { - if let result = data["concurrent"] as! [String: Any]? { - self.kalturaPluginManager.reportConcurrencyEvent() - } - } - + guard let data = response.data as? String, data.lowercased() == "concurrent" else { return } + self.kalturaPluginManager.reportConcurrencyEvent() } } - USRExecutor.shared.send(request: builder.build()) - } } diff --git a/Plugins/Youbora/YouboraEvent.swift b/Plugins/Youbora/YouboraEvent.swift index 98b49caa..3dc428c6 100644 --- a/Plugins/Youbora/YouboraEvent.swift +++ b/Plugins/Youbora/YouboraEvent.swift @@ -8,9 +8,12 @@ import UIKit -public class YouboraReportSent : PKEvent { - public let message: String - public init(message: String) { - self.message = message +public class YouboraEvent: PKEvent { + class YouboraReportSent : YouboraEvent { + convenience init(message: NSString) { + self.init(["message" : message]) + } } + + public static let youboraReportSent: YouboraEvent.Type = YouboraReportSent.self } diff --git a/Plugins/Youbora/YouboraManager.swift b/Plugins/Youbora/YouboraManager.swift index 5bf1e42b..ece7d46d 100644 --- a/Plugins/Youbora/YouboraManager.swift +++ b/Plugins/Youbora/YouboraManager.swift @@ -15,40 +15,46 @@ import AVKit class YouboraManager: YBPluginGeneric { - private var pkPlayer: Player! - private var mediaEntry: MediaEntry! + private weak var pkPlayer: Player? + weak var mediaEntry: MediaEntry? public var currentBitrate: Double? - init!(options: NSObject!, player: Player, media: MediaEntry) { + // for some reason we must implement the initializer this way because the way youbora implemented the init. + // this means player and media entry are defined as optionals but they must have values when initialized. + // All the checks for optionals in this class are just because we defined them as optionals but they are not so the checks are irrelevant. + init!(options: NSObject!, player: Player, mediaEntry: MediaEntry) { super.init(options: options) self.pkPlayer = player - self.mediaEntry = media + self.mediaEntry = mediaEntry } - override init() { + private override init() { super.init() } - //MARK: Override methods - override func getMediaDuration() -> NSNumber! { - return pkPlayer.duration as NSNumber! + /************************************************************/ + // MARK: - Overrides + /************************************************************/ + + override func getMediaDuration() -> NSNumber { + return NSNumber(value: pkPlayer?.duration ?? 0) } - override func getResource() -> String! { + override func getResource() -> String { PKLog.trace("Resource") - return self.mediaEntry.id + return self.mediaEntry?.id ?? "" } - override func getPlayhead() -> NSNumber! { - let currentTIme = self.pkPlayer.currentTime - return currentTIme as NSNumber! + override func getPlayhead() -> NSNumber { + let currentTIme = self.pkPlayer?.currentTime ?? 0 + return NSNumber(value: currentTIme) } - override func getPlayerVersion() -> String! { - return "PlayKit-0.1.0" + override func getPlayerVersion() -> String { + return "PlayKit-\(PlayKitManager.versionString)" } - override func getBitrate() -> NSNumber! { + override func getBitrate() -> NSNumber { if let bitrate = currentBitrate { return NSNumber(value: bitrate) } diff --git a/Plugins/Youbora/YouboraPlugin.swift b/Plugins/Youbora/YouboraPlugin.swift index 7997a040..25202b33 100644 --- a/Plugins/Youbora/YouboraPlugin.swift +++ b/Plugins/Youbora/YouboraPlugin.swift @@ -3,7 +3,7 @@ // AdvancedExample // // Created by Oded Klein on 19/10/2016. -// Copyright © 2016 Google, Inc. All rights reserved. +// Copyright © 2016 Kaltura, Inc. All rights reserved. // import YouboraLib @@ -12,187 +12,166 @@ import AVFoundation public class YouboraPlugin: PKPlugin { - private var player: Player! - private var messageBus: MessageBus? - private var config: AnalyticsConfig! - private var mediaEntry: MediaEntry! - - private var youboraManager : YouboraManager! - public static var pluginName: String = "YouboraPlugin" - + private unowned var player: Player + private unowned var messageBus: MessageBus + private var config: AnalyticsConfig? + private var youboraManager : YouboraManager? private var isFirstPlay = true - required public init() { - - } + public static var pluginName: String = "YouboraPlugin" + public weak var mediaEntry: MediaEntry? - public func load(player: Player, mediaConfig: MediaEntry, pluginConfig: Any?, messageBus: MessageBus) { + /************************************************************/ + // MARK: - PKPlugin + /************************************************************/ + public required init(player: Player, pluginConfig: Any?, messageBus: MessageBus) { + self.player = player self.messageBus = messageBus - self.mediaEntry = mediaConfig - if let aConfig = pluginConfig as? AnalyticsConfig { self.config = aConfig - self.player = player } else { PKLog.warning("There is no Analytics Config.") } - - setupOptions() - - registerToAllEvents() - - startMonitoring(player: player) + } + + public func onLoad(mediaConfig: MediaConfig) { + PKLog.trace("plugin \(type(of:self)) onLoad with media config: \(mediaConfig)") + self.mediaEntry = mediaConfig.mediaEntry + self.setupYouboraManager() { succeeded in + if succeeded { + self.registerToAllEvents() + self.startMonitoring(player: self.player) + } + } + } + + public func onUpdateMedia(mediaConfig: MediaConfig) { + PKLog.trace("plugin \(type(of:self)) onLoad with media config: \(mediaConfig)") + self.mediaEntry = mediaConfig.mediaEntry + self.setupYouboraManager() } public func destroy() { - stopMonitoring() + self.stopMonitoring() } - private func setupOptions() { - let options = self.config.params - if var media = options?["media"] as? [String: Any] { - if let entry = self.mediaEntry { - media["resource"] = entry.id - media["title"] = entry.id - media["duration"] = self.player.duration - - } else { - PKLog.warning("There is no MediaEntry") - } + /************************************************************/ + // MARK: - Private + /************************************************************/ + + private func setupYouboraManager(completionHandler: ((_ succeeded: Bool) -> Void)? = nil) { + if let config = self.config, var media = config.params["media"] as? [String : Any], let mediaEntry = self.mediaEntry { + media["resource"] = mediaEntry.id + media["title"] = mediaEntry.id + media["duration"] = self.player.duration + config.params["media"] = media + youboraManager = YouboraManager(options: config.params as NSObject!, player: player, mediaEntry: mediaEntry) + completionHandler?(true) + } else { + PKLog.warning("There is no config params or MediaEntry, could not setup youbora manager") + completionHandler?(false) } - youboraManager = YouboraManager(options: options as NSObject!, player: player, media: self.mediaEntry) - } private func startMonitoring(player: Player) { + guard let youboraManager = self.youboraManager else { return } PKLog.trace("Start monitoring using Youbora") youboraManager.startMonitoring(withPlayer: youboraManager) } private func stopMonitoring() { + guard let youboraManager = self.youboraManager else { return } PKLog.trace("Stop monitoring using Youbora") youboraManager.stopMonitoring() } private func registerToAllEvents() { + PKLog.trace("register to all events") - PKLog.trace() - - self.messageBus?.addObserver(self, events: [PlayerEvents.canPlay.self], block: { (info) in - PKLog.trace("canPlay info: \(info)") - - self.postEventLogWithMessage(message: "Event info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.canPlay], block: { [unowned self] event in + self.postEventLogWithMessage(message: "canPlay event: \(event)") }) - self.messageBus?.addObserver(self, events: [PlayerEvents.play.self], block: { (info) in - PKLog.trace("play info: \(info)") - self.youboraManager.playHandler() - self.postEventLogWithMessage(message: "Event info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.play], block: { [unowned self] event in + guard let youboraManager = self.youboraManager else { return } + youboraManager.playHandler() + self.postEventLogWithMessage(message: "play event: \(event)") }) - self.messageBus?.addObserver(self, events: [PlayerEvents.playing.self], block: { (info) in - PKLog.trace("playing info: \(info)") - self.postEventLogWithMessage(message: "Event info: \(info)") - + self.messageBus.addObserver(self, events: [PlayerEvent.playing], block: { [unowned self] event in + self.postEventLogWithMessage(message: "playing event: \(event)") + + guard let youboraManager = self.youboraManager else { return } if self.isFirstPlay { - - //let timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(YouboraPlugin.didStartPlaying), userInfo: nil, repeats: false) - //timer.fire() - - self.youboraManager.joinHandler() - self.youboraManager.bufferedHandler() + youboraManager.joinHandler() + youboraManager.bufferedHandler() self.isFirstPlay = false } else { - self.youboraManager.resumeHandler() + youboraManager.resumeHandler() } }) - self.messageBus?.addObserver(self, events: [PlayerEvents.pause.self], block: { (info) in - PKLog.trace("pause info: \(info)") - self.youboraManager.pauseHandler() - self.postEventLogWithMessage(message: "Event info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.pause], block: { [unowned self] event in + guard let youboraManager = self.youboraManager else { return } + youboraManager.pauseHandler() + self.postEventLogWithMessage(message: "pause event: \(event)") }) - self.messageBus?.addObserver(self, events: [PlayerEvents.seeking.self], block: { (info) in - PKLog.trace("seeking info: \(info)") - self.youboraManager.seekingHandler() - - self.postEventLogWithMessage(message: "Event info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.seeking], block: { [unowned self] event in + guard let youboraManager = self.youboraManager else { return } + youboraManager.seekingHandler() + self.postEventLogWithMessage(message: "seeking event: \(event)") }) - self.messageBus?.addObserver(self, events: [PlayerEvents.seeked.self], block: { (info) in - PKLog.trace("seeked info: \(info)") - self.youboraManager.seekedHandler() - - self.postEventLogWithMessage(message: "Event info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.seeked], block: { [unowned self] (event) in + guard let youboraManager = self.youboraManager else { return } + youboraManager.seekedHandler() + self.postEventLogWithMessage(message: "seeked event: \(event)") }) - self.messageBus?.addObserver(self, events: [PlayerEvents.ended.self], block: { (info) in - PKLog.trace("ended info: \(info)") - self.youboraManager.endedHandler() - - self.postEventLogWithMessage(message: "Event info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.ended], block: { [unowned self] (event) in + guard let youboraManager = self.youboraManager else { return } + youboraManager.endedHandler() + self.postEventLogWithMessage(message: "ended event: \(event)") }) - self.messageBus?.addObserver(self, events: [PlayerEvents.playbackParamsUpdated.self], block: { (info) in - PKLog.trace("playbackParamsUpdated info: \(info)") - if let paramsEvent = info as? PlayerEvents.playbackParamsUpdated { - self.youboraManager.currentBitrate = paramsEvent.currentBitrate - } - self.postEventLogWithMessage(message: "Event info: \(info)") + self.messageBus.addObserver(self, events: [PlayerEvent.playbackParamsUpdated], block: { [unowned self] (event) in + guard let youboraManager = self.youboraManager else { return } + youboraManager.currentBitrate = event.currentBitrate?.doubleValue + self.postEventLogWithMessage(message: "playbackParamsUpdated event: \(event)") }) - self.player.addObserver(self, events: [PlayerEvents.stateChanged.self]) { (data: Any) in - - if let stateChanged = data as? PlayerEvents.stateChanged { - - switch stateChanged.newSate { + self.messageBus.addObserver(self, events: [PlayerEvent.stateChanged]) { [unowned self] (event) in + guard let youboraManager = self.youboraManager else { return } + if let stateChanged = event as? PlayerEvent.StateChanged { + switch event.newState { case .buffering: - self.youboraManager.bufferingHandler() - self.postEventLogWithMessage(message: "Event info: Buffering") - break - default: - + youboraManager.bufferingHandler() + self.postEventLogWithMessage(message: "Buffering event: ֿ\(event)") break + default: break } - switch stateChanged.oldSate { + switch event.oldState { case .buffering: - self.youboraManager.bufferedHandler() - self.postEventLogWithMessage(message: "Event info: Buffered") - break - default: - + youboraManager.bufferedHandler() + self.postEventLogWithMessage(message: "Buffered event: \(event)") break + default: break } } - - } - self.messageBus?.addObserver(self, events: AdEvents.allEventTypes, block: { (info) in - - PKLog.trace("Ads event info: \(info)") - - self.postEventLogWithMessage(message: "Event info: \(info)") + self.messageBus.addObserver(self, events: AdEvent.allEventTypes, block: { [unowned self] (event) in + self.postEventLogWithMessage(message: "Ads event event: \(event)") }) } private func postEventLogWithMessage(message: String) { - let eventLog = YouboraReportSent(message: message) - self.messageBus?.post(eventLog) - } - - @objc private func didStartPlaying() { - PKLog.trace("didStartPlaying") - self.youboraManager.joinHandler() + PKLog.trace(message) + let eventLog = YouboraEvent.YouboraReportSent(message: message as NSString) + self.messageBus.post(eventLog) } } - - - - - - diff --git a/README.md b/README.md index ebc09948..4b70526a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ +[![CI Status](https://api.travis-ci.org/kaltura/playkit-ios.svg?branch=develop)](https://travis-ci.org/kaltura/playkit-ios) +[![Version](https://img.shields.io/cocoapods/v/PlayKit.svg?style=flat)](https://cocoapods.org/pods/PlayKit) +[![License](https://img.shields.io/cocoapods/l/PlayKit.svg?style=flat)](https://cocoapods.org/pods/PlayKit) +[![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.* +## Overview The **Kaltura Player SDK** is fully native and introduces significant performance improvements. The SDK is intended to be integrated in any iOS application and includes the following features: * Online and Offline Playback @@ -15,9 +22,8 @@ The **Kaltura Player SDK** is fully native and introduces significant performanc * Youbora * Chromecast and AirPlay support -Further documentation is coming soon. - -[![CI Status](http://img.shields.io/travis/kaltura/playkit-ios.svg?style=flat)](https://travis-ci.org/kaltura/playkit-ios) +## Usage Guides +Please see our [VPaaS Documentation site](https://vpaas.kaltura.com/documentation/Mobile-Video-Player-SDKs/v3_iOS_Introduction.html). ## License and Copyright Information All code in this project is released under the [AGPLv3 license](http://www.gnu.org/licenses/agpl-3.0.html) unless a different license for a particular library is specified in the applicable library path. diff --git a/Widevine/WidevineClassicAssetHandler.swift b/Widevine/WidevineClassicAssetHandler.swift index d14793e8..6c99038e 100644 --- a/Widevine/WidevineClassicAssetHandler.swift +++ b/Widevine/WidevineClassicAssetHandler.swift @@ -8,7 +8,10 @@ import Foundation import AVFoundation + +#if WIDEVINE_ENABLED import PlayKitWV +#endif class WidevineClassicAssetHandler: AssetHandler { @@ -55,7 +58,8 @@ class WidevineClassicAssetHandler: AssetHandler { } PKLog.trace("playAsset:: url: \(contentUrl.absoluteString), uri: \(licenseUri.absoluteString)") - + + #if WIDEVINE_ENABLED WidevineClassicCDM.playAsset(contentUrl.absoluteString, withLicenseUri: licenseUri.absoluteString) { (_ playbackURL:String?)->Void in guard let playbackURL = playbackURL else { @@ -69,6 +73,7 @@ class WidevineClassicAssetHandler: AssetHandler { readyCallback(nil, AVURLAsset(url: URL(string: playbackURL)!)) } } + #endif } required init() {}