diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index fa917b1f..3e8df098 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -8,8 +8,6 @@ public class ApiVideoPlayerController: NSObject { private let avPlayer = AVPlayer(playerItem: nil) private let offSubtitleLanguage = SubtitleLanguage(language: "Off", code: nil) private var analytics: PlayerAnalytics? - private let videoType: VideoType - private let videoId: String private var playerManifest: PlayerManifest! private var timeObserver: Any? private var isFirstPlay = true @@ -17,24 +15,20 @@ public class ApiVideoPlayerController: NSObject { private let taskExecutor: TasksExecutorProtocol.Type #if !os(macOS) public convenience init( - videoId: String, - videoType: VideoType, + videoOptions: VideoOptions?, playerLayer: AVPlayerLayer, events: PlayerEvents? = nil ) { - self.init(videoId: videoId, videoType: videoType, events: events) + self.init(videoOptions: videoOptions, events: events) playerLayer.player = self.avPlayer } #endif public init( - videoId: String, - videoType: VideoType, + videoOptions: VideoOptions?, events: PlayerEvents?, taskExecutor: TasksExecutorProtocol.Type = TasksExecutor.self ) { - self.videoId = videoId - self.videoType = videoType self.taskExecutor = taskExecutor super.init() self.avPlayer.addObserver( @@ -43,20 +37,25 @@ public class ApiVideoPlayerController: NSObject { options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil ) + self.avPlayer.addObserver( + self, + forKeyPath: "currentItem.presentationSize", + options: NSKeyValueObservingOptions.new, + context: nil + ) if let events = events { self.addEvents(events: events) } - self.getPlayerJSON(videoType: videoType) { error in - if let error = error { - self.notifyError(error: error) - } + defer { + self.videoOptions = videoOptions } } - private func getVideoUrl(videoType: VideoType, videoId: String, privateToken: String? = nil) -> String { + private func getVideoUrl(videoOptions: VideoOptions) -> String { + let privateToken: String? = nil var baseUrl = "" - if videoType == .vod { + if videoOptions.videoType == .vod { baseUrl = "https://cdn.api.video/vod/" } else { baseUrl = "https://live.api.video/" @@ -64,13 +63,13 @@ public class ApiVideoPlayerController: NSObject { var url: String! if let privateToken = privateToken { - url = baseUrl + "\(videoId)/token/\(privateToken)/player.json" - } else { url = baseUrl + "\(videoId)/player.json" } + url = baseUrl + "\(videoOptions.videoId)/token/\(privateToken)/player.json" + } else { url = baseUrl + "\(videoOptions.videoId)/player.json" } return url } - private func getPlayerJSON(videoType: VideoType, completion: @escaping (Error?) -> Void) { - let url = self.getVideoUrl(videoType: videoType, videoId: self.videoId) + private func getPlayerJSON(videoOptions: VideoOptions, completion: @escaping (Error?) -> Void) { + let url = self.getVideoUrl(videoOptions: videoOptions) guard let path = URL(string: url) else { completion(PlayerError.urlError("Couldn't set up url from this videoId")) return @@ -182,8 +181,8 @@ public class ApiVideoPlayerController: NSObject { } catch { print("error with the url") } } - public func isPlaying() -> Bool { - return self.avPlayer.isPlaying() + public var isPlaying: Bool { + return self.avPlayer.isPlaying } public func play() { @@ -241,6 +240,19 @@ public class ApiVideoPlayerController: NSObject { } } + public var videoOptions: VideoOptions? { + didSet { + guard let videoOptions = videoOptions else { + return + } + self.getPlayerJSON(videoOptions: videoOptions) { error in + if let error = error { + self.notifyError(error: error) + } + } + } + } + public var isMuted: Bool { get { self.avPlayer.isMuted @@ -285,11 +297,15 @@ public class ApiVideoPlayerController: NSObject { self.duration.roundedSeconds == self.currentTime.roundedSeconds } - var hasSubtitles: Bool { + public var videoSize: CGSize { + self.avPlayer.videoSize + } + + public var hasSubtitles: Bool { self.subtitles.count > 1 } - var subtitles: [SubtitleLanguage] { + public var subtitles: [SubtitleLanguage] { var subtitles: [SubtitleLanguage] = [offSubtitleLanguage] if let playerItem = avPlayer.currentItem, let group = playerItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) @@ -301,7 +317,7 @@ public class ApiVideoPlayerController: NSObject { return subtitles } - var currentSubtitle: SubtitleLanguage { + public var currentSubtitle: SubtitleLanguage { get { if let playerItem = avPlayer.currentItem, let group = playerItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible), @@ -338,7 +354,7 @@ public class ApiVideoPlayerController: NSObject { } #endif - func hideSubtitle() { + public func hideSubtitle() { guard let currentItem = self.avPlayer.currentItem else { return } if let group = currentItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) { currentItem.select(nil, in: group) @@ -462,9 +478,17 @@ public class ApiVideoPlayerController: NSObject { self.doTimeControlStatus() } } + if keyPath == "currentItem.presentationSize" { + guard let change = change else { return } + guard let newSize = change[.newKey] as? CGSize else { return } + for events in self.events { + events.didVideoSizeChanged?(newSize) + } + } } deinit { + avPlayer.removeObserver(self, forKeyPath: "currentItem.presentationSize", context: nil) avPlayer.removeObserver(self, forKeyPath: "timeControlStatus", context: nil) avPlayer.currentItem?.removeObserver(self, forKeyPath: "status", context: nil) NotificationCenter.default.removeObserver(self) @@ -472,10 +496,16 @@ public class ApiVideoPlayerController: NSObject { } extension AVPlayer { - @available(iOS 10.0, *) - func isPlaying() -> Bool { + @available(iOS 10.0, *) var isPlaying: Bool { return (rate != 0 && error == nil) } + + var videoSize: CGSize { + guard let size = self.currentItem?.presentationSize else { + return CGSize(width: 0, height: 0) + } + return size + } } enum PlayerError: Error { diff --git a/Sources/ApiVideoPlayer/PlayerEvents.swift b/Sources/ApiVideoPlayer/PlayerEvents.swift index 98ee55fb..c5546936 100644 --- a/Sources/ApiVideoPlayer/PlayerEvents.swift +++ b/Sources/ApiVideoPlayer/PlayerEvents.swift @@ -12,6 +12,7 @@ public class PlayerEvents { public var didSeek: ((_ from: CMTime, _ to: CMTime) -> Void)? public var didEnd: (() -> Void)? public var didError: ((_ error: Error) -> Void)? + public var didVideoSizeChanged: ((_ size: CGSize) -> Void)? public init( didPrepare: (() -> Void)? = nil, @@ -24,7 +25,8 @@ public class PlayerEvents { didSetVolume: ((Float) -> Void)? = nil, didSeek: ((CMTime, CMTime) -> Void)? = nil, didEnd: (() -> Void)? = nil, - didError: ((Error) -> Void)? = nil + didError: ((Error) -> Void)? = nil, + didVideoSizeChanged: ((CGSize) -> Void)? = nil ) { self.didPrepare = didPrepare self.didPause = didPause @@ -37,5 +39,6 @@ public class PlayerEvents { self.didSeek = didSeek self.didEnd = didEnd self.didError = didError + self.didVideoSizeChanged = didVideoSizeChanged } } diff --git a/Sources/ApiVideoPlayer/SubtitleLanguage.swift b/Sources/ApiVideoPlayer/SubtitleLanguage.swift index 6c5a8bf6..e9a37367 100644 --- a/Sources/ApiVideoPlayer/SubtitleLanguage.swift +++ b/Sources/ApiVideoPlayer/SubtitleLanguage.swift @@ -1,6 +1,6 @@ import Foundation -struct SubtitleLanguage { +public struct SubtitleLanguage { public var language: String public var code: String? diff --git a/Sources/ApiVideoPlayer/VideoOptions.swift b/Sources/ApiVideoPlayer/VideoOptions.swift new file mode 100644 index 00000000..cb635c52 --- /dev/null +++ b/Sources/ApiVideoPlayer/VideoOptions.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct VideoOptions { + public var videoId: String + public var videoType: VideoType + + /* only .vod is supported */ + public init(videoId: String, videoType: VideoType = VideoType.vod) { + self.videoId = videoId + self.videoType = videoType + } +} + +public enum VideoType: String { + case vod + case live +} diff --git a/Sources/ApiVideoPlayer/VideoType.swift b/Sources/ApiVideoPlayer/VideoType.swift deleted file mode 100644 index 2d1ce17c..00000000 --- a/Sources/ApiVideoPlayer/VideoType.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation -public enum VideoType: String { - case vod - case live -} diff --git a/Sources/ApiVideoPlayer/View/ApiVideoPlayerView.swift b/Sources/ApiVideoPlayer/View/ApiVideoPlayerView.swift index 631a6d67..bc3994b7 100644 --- a/Sources/ApiVideoPlayer/View/ApiVideoPlayerView.swift +++ b/Sources/ApiVideoPlayer/View/ApiVideoPlayerView.swift @@ -23,18 +23,36 @@ public class ApiVideoPlayerView: UIView { /// - videoId: Need videoid to display the video. /// - videoType: VideoType object to display vod or live controls. Only vod is supported yet. /// - events: Callback to get all the player events. - public init( + public convenience init( frame: CGRect, videoId: String, videoType: VideoType, hideControls: Bool = false, events: PlayerEvents? = nil + ) { + self.init( + frame: frame, + videoOptions: VideoOptions(videoId: videoId, videoType: videoType), + hideControls: hideControls, + events: events + ) + } + + /// Init method for PlayerView. + /// - Parameters: + /// - frame: frame of theplayer view. + /// - videoOption: The video option containing the videoId and the videoType + /// - events: Callback to get all the player events. + public init( + frame: CGRect, + videoOptions: VideoOptions, + hideControls: Bool = false, + events: PlayerEvents? = nil ) { self.userEvents = events self.isHidenControls = hideControls self.playerController = ApiVideoPlayerController( - videoId: videoId, - videoType: videoType, + videoOptions: videoOptions, playerLayer: self.playerLayer, events: events ) @@ -81,10 +99,19 @@ public class ApiVideoPlayerView: UIView { self.playerLayer.frame = bounds } + public var videoOptions: VideoOptions? { + get { + self.playerController.videoOptions + } + set { + self.playerController.videoOptions = newValue + } + } + /// Get information if the video is playing. /// - Returns: Boolean. - public func isPlaying() -> Bool { - return self.playerController.isPlaying() + public var isPlaying: Bool { + return self.playerController.isPlaying } /// Play the video. diff --git a/Sources/ApiVideoPlayer/View/VodControlsView.swift b/Sources/ApiVideoPlayer/View/VodControlsView.swift index de6567d3..fc4d54ed 100644 --- a/Sources/ApiVideoPlayer/View/VodControlsView.swift +++ b/Sources/ApiVideoPlayer/View/VodControlsView.swift @@ -282,7 +282,7 @@ class VodControlsView: UIView, UIGestureRecognizerDelegate { @objc func playPauseAction() { self.resetTimer() - if !self.playerController.isPlaying() { + if !self.playerController.isPlaying { // Detects end of playing if self.playerController.isAtEnd { self.playerController.replay() @@ -385,7 +385,7 @@ class VodControlsView: UIView, UIGestureRecognizerDelegate { switch touchEvent.phase { case .began: // handle drag began - if self.playerController.isPlaying() { + if self.playerController.isPlaying { // Avoid to trigger callbacks and analytics when the user uses the seek slider self.playerController.pauseBeforeSeek() self.sliderDidPauseVideo = true diff --git a/Sources/ApiVideoPlayer/ViewController/ApiVideoPlayer.swift b/Sources/ApiVideoPlayer/ViewController/ApiVideoPlayer.swift index 437813f0..a18d6528 100644 --- a/Sources/ApiVideoPlayer/ViewController/ApiVideoPlayer.swift +++ b/Sources/ApiVideoPlayer/ViewController/ApiVideoPlayer.swift @@ -6,7 +6,11 @@ public struct ApiVideoPlayer: UIViewControllerRepresentable { private let playerViewController: SwiftUIPlayerViewController public init(videoId: String, videoType: VideoType, events: PlayerEvents? = nil) { - self.playerViewController = SwiftUIPlayerViewController(videoId: videoId, videoType: videoType, events: events) + self.init(videoOptions: VideoOptions(videoId: videoId, videoType: videoType), events: events) + } + + public init(videoOptions: VideoOptions, events: PlayerEvents? = nil) { + self.playerViewController = SwiftUIPlayerViewController(videoOptions: videoOptions, events: events) } public func makeUIViewController(context _: Context) -> SwiftUIPlayerViewController { @@ -23,8 +27,8 @@ public struct ApiVideoPlayer: UIViewControllerRepresentable { self.playerViewController.pause() } - public func isPlaying() -> Bool { - return self.playerViewController.isPlaying() + public var isPlaying: Bool { + return self.playerViewController.isPlaying } public func replay() { diff --git a/Sources/ApiVideoPlayer/ViewController/SwiftUIPlayerViewController.swift b/Sources/ApiVideoPlayer/ViewController/SwiftUIPlayerViewController.swift index 7566e814..3b44e895 100644 --- a/Sources/ApiVideoPlayer/ViewController/SwiftUIPlayerViewController.swift +++ b/Sources/ApiVideoPlayer/ViewController/SwiftUIPlayerViewController.swift @@ -11,11 +11,10 @@ public class SwiftUIPlayerViewController: UIViewController { fatalError("init(coder:) is not supported") } - init(videoId: String, videoType: VideoType, events: PlayerEvents? = nil) { + init(videoOptions: VideoOptions, events: PlayerEvents? = nil) { self.playerView = ApiVideoPlayerView( frame: .zero, - videoId: videoId, - videoType: videoType /* only .vod is supported */, + videoOptions: videoOptions, events: events ) super.init(nibName: nil, bundle: nil) @@ -50,8 +49,8 @@ public class SwiftUIPlayerViewController: UIViewController { self.playerView.pause() } - public func isPlaying() -> Bool { - return self.playerView.isPlaying() + public var isPlaying: Bool { + return self.playerView.isPlaying } public func replay() { diff --git a/Tests/ApiVideoPlayerTests/IntegrationTests/ApiVideoPlayerControllerIntegrationTests.swift b/Tests/ApiVideoPlayerTests/IntegrationTests/ApiVideoPlayerControllerIntegrationTests.swift index e75a313e..876acd97 100644 --- a/Tests/ApiVideoPlayerTests/IntegrationTests/ApiVideoPlayerControllerIntegrationTests.swift +++ b/Tests/ApiVideoPlayerTests/IntegrationTests/ApiVideoPlayerControllerIntegrationTests.swift @@ -23,7 +23,40 @@ final class ApiVideoPlayerControllerIntegrationTests: XCTestCase { errorExpectation.fulfill() } ) - let controller = ApiVideoPlayerController(videoId: VideoId.validVideoId, videoType: .vod, events: events) + let controller = ApiVideoPlayerController( + videoOptions: VideoOptions(videoId: VideoId.validVideoId), + events: events + ) + wait(for: [completedExpectationPrepare], timeout: 10) + controller.play() + wait(for: [completedExpectationPlay], timeout: 2) + wait(for: [errorExpectation], timeout: 5) + } + + func testValidVideoIdWithSetterPlay() throws { + let completedExpectationPrepare = expectation(description: "Completed Prepare") + let completedExpectationPlay = expectation(description: "Completed Play") + let errorExpectation = expectation(description: "error is called") + errorExpectation.isInverted = true + let events = PlayerEvents( + didPrepare: { () in + print("ready") + completedExpectationPrepare.fulfill() + }, + didPlay: { () in + print("play") + completedExpectationPlay.fulfill() + }, + didError: { error in + print("error\(error)") + errorExpectation.fulfill() + } + ) + let controller = ApiVideoPlayerController( + videoOptions: nil, + events: events + ) + controller.videoOptions = VideoOptions(videoId: VideoId.validVideoId) wait(for: [completedExpectationPrepare], timeout: 10) controller.play() wait(for: [completedExpectationPlay], timeout: 2) @@ -54,7 +87,10 @@ final class ApiVideoPlayerControllerIntegrationTests: XCTestCase { errorExpectation.fulfill() } ) - let controller = ApiVideoPlayerController(videoId: VideoId.validVideoId, videoType: .vod, events: events) + let controller = ApiVideoPlayerController( + videoOptions: VideoOptions(videoId: VideoId.validVideoId), + events: events + ) wait(for: [completedExpectationPrepare], timeout: 10) controller.play() wait(for: [completedExpectationPlay], timeout: 2) @@ -91,7 +127,10 @@ final class ApiVideoPlayerControllerIntegrationTests: XCTestCase { errorExpectation.fulfill() } ) - let controller = ApiVideoPlayerController(videoId: VideoId.validVideoId, videoType: .vod, events: events) + let controller = ApiVideoPlayerController( + videoOptions: VideoOptions(videoId: VideoId.validVideoId), + events: events + ) wait(for: [completedExpectationPrepare], timeout: 10) controller.play() controller.pause() @@ -113,7 +152,34 @@ final class ApiVideoPlayerControllerIntegrationTests: XCTestCase { errorExpectation.fulfill() } ) - let controller = ApiVideoPlayerController(videoId: VideoId.validVideoId, videoType: .vod, events: events) + let controller = ApiVideoPlayerController( + videoOptions: VideoOptions(videoId: VideoId.validVideoId), + events: events + ) + waitForExpectations(timeout: 10, handler: nil) + XCTAssertEqual(controller.duration.seconds, 60.2) + } + + func testWithVideoOptionsWithSetterDuration() throws { + let prepareExpectation = self.expectation(description: "prepare is called") + let errorExpectation = self.expectation(description: "error is called") + errorExpectation.isInverted = true + let events = PlayerEvents( + didPrepare: { () in + print("ready") + prepareExpectation.fulfill() + }, + didError: { error in + print("error : \(error)") + errorExpectation.fulfill() + } + ) + let controller = ApiVideoPlayerController( + videoOptions: nil, + events: events + ) + + controller.videoOptions = VideoOptions(videoId: VideoId.validVideoId) waitForExpectations(timeout: 10, handler: nil) XCTAssertEqual(controller.duration.seconds, 60.2) } @@ -132,8 +198,7 @@ final class ApiVideoPlayerControllerIntegrationTests: XCTestCase { errorExpectation.fulfill() } ) - _ = ApiVideoPlayerController(videoId: VideoId.invalidVideoId, videoType: .vod, events: events) + _ = ApiVideoPlayerController(videoOptions: VideoOptions(videoId: VideoId.invalidVideoId), events: events) waitForExpectations(timeout: 10, handler: nil) } - } diff --git a/Tests/ApiVideoPlayerTests/UnitTests/ApiVideoPlayerUnitTests.swift b/Tests/ApiVideoPlayerTests/UnitTests/ApiVideoPlayerUnitTests.swift index 0b200b32..673594ff 100644 --- a/Tests/ApiVideoPlayerTests/UnitTests/ApiVideoPlayerUnitTests.swift +++ b/Tests/ApiVideoPlayerTests/UnitTests/ApiVideoPlayerUnitTests.swift @@ -36,8 +36,7 @@ class ApiVideoPlayerUnitTests: XCTestCase { self.generateRessource(ressource: "responseSuccess") _ = ApiVideoPlayerController( - videoId: "vi18RL1kvZlDRdzk7Mas59HT", - videoType: .vod, + videoOptions: VideoOptions(videoId: "vi18RL1kvZlDRdzk7Mas59HT"), events: events, taskExecutor: MockedTasksExecutor.self ) @@ -62,8 +61,7 @@ class ApiVideoPlayerUnitTests: XCTestCase { self.generateRessource(ressource: "responseError") _ = ApiVideoPlayerController( - videoId: "vi18RL1kvZlDRdzk7Mas59HT", - videoType: .vod, + videoOptions: VideoOptions(videoId: "vi18RL1kvZlDRdzk7Mas59HT"), events: events, taskExecutor: MockedTasksExecutor.self ) @@ -87,8 +85,7 @@ class ApiVideoPlayerUnitTests: XCTestCase { } ) _ = ApiVideoPlayerController( - videoId: "vi18RL1kvZlDRdzk7Mas59HT", - videoType: .vod, + videoOptions: VideoOptions(videoId: "vi18RL1kvZlDRdzk7Mas59HT"), events: events, taskExecutor: MockedTasksExecutor.self )