From 934f2b39ea22e26c2449cee8d80ac235bdf9bf02 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 18 Apr 2023 12:08:53 +0200 Subject: [PATCH 01/55] feat() add mediaplayer controller to control video from lockscreen --- .../PlayerUIKit/PlayerUIkit/AppDelegate.swift | 1 + Examples/PlayerUIKit/PlayerUIkit/Info.plist | 4 + .../PlayerUIkit/PlayerViewController.swift | 14 +++ .../ApiVideoPlayerController.swift | 90 +++++++++++++++++++ .../Request/RequestsBuilder.swift | 21 +++++ .../Views/ApiVideoPlayerView.swift | 4 + 6 files changed, 134 insertions(+) diff --git a/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift b/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift index e585210d..8e827530 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift +++ b/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift @@ -7,6 +7,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Override point for customization after application launch. + UIApplication.shared.beginReceivingRemoteControlEvents() return true } diff --git a/Examples/PlayerUIKit/PlayerUIkit/Info.plist b/Examples/PlayerUIKit/PlayerUIkit/Info.plist index dd3c9afd..8e6b5a88 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/Info.plist +++ b/Examples/PlayerUIKit/PlayerUIkit/Info.plist @@ -21,5 +21,9 @@ + UIBackgroundModes + + audio + diff --git a/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift b/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift index 4c583d98..b403acef 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift +++ b/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift @@ -125,6 +125,8 @@ class PlayerViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.addSubview(self.scrollView) + self.becomeFirstResponder() + self.scrollView.addSubview(self.playerView) self.playerView.addDelegate(self) let doubleTap = UITapGestureRecognizer(target: self, action: #selector(self.handleDoubleTap(_:))) @@ -183,6 +185,18 @@ class PlayerViewController: UIViewController { self.playerView.viewController = self } + override var canBecomeFirstResponder: Bool { + return true + } + + override func remoteControlReceived(with event: UIEvent?) { + if let event = event { + if event.type == .remoteControl { + self.playerView.remoteControlEventReceived(with: event) + } + } + } + private func constraints() { // ScrollView self.scrollView.translatesAutoresizingMaskIntoConstraints = false diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 009877ae..d29d09b7 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -2,6 +2,7 @@ import ApiVideoPlayerAnalytics import AVFoundation import AVKit import Foundation +import MediaPlayer /// The ApiVideoPlayerController class is a wrapper around ``AVPlayer``. /// It is used internally of the ``ApiVideoPlayerView``. @@ -516,13 +517,53 @@ public class ApiVideoPlayerController: NSObject { self.multicastDelegate.didPause() } + private func loadThumabnail(completion: @escaping (UIImage?) -> Void) { + if let thumbnailUrl = self.videoOptions?.thumbnailUrl { + if let url = URL(string: thumbnailUrl) { + RequestsBuilder.getThumbnailImage(taskExecutor: self.taskExecutor, url: url, completion: { data in + completion(UIImage(data: data)) + }, didError: { error in + self.multicastDelegate.didError(error) + }) + } else { + self.multicastDelegate.didError(PlayerError.urlError("Error on URL with this thumbnail string url")) + } + } else { + self.multicastDelegate.didError(PlayerError.urlError("can not get thumbnail from this url")) + } + } + private func doPlayAction() { if self.isSeeking { self.isSeeking = false return } + let infoCenter = MPNowPlayingInfoCenter.default() if self.isFirstPlay { self.isFirstPlay = false + // TODO: add MPNowPlayingInfoCenter + + // Configure Now Playing Info + let infoCenter = MPNowPlayingInfoCenter.default() + + loadThumabnail(completion: { image in + let artwork = MPMediaItemArtwork(boundsSize: image!.size, requestHandler: { _ -> UIImage in + return image ?? UIImage() + }) + + // Set the title of the video as the Now Playing Info title + let nowPlayingInfo: [String: Any] = [ + MPMediaItemPropertyArtwork: artwork, + MPMediaItemPropertyPlaybackDuration: self.duration.seconds, + MPNowPlayingInfoPropertyElapsedPlaybackTime: self.currentTime.seconds + ] + print("currentime second first play: \(self.currentTime.seconds)") +// nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.currentTime.seconds +// nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = self.duration.seconds + infoCenter.nowPlayingInfo = nowPlayingInfo + }) + // setupCommandCenter() + self.analytics?.play { result in switch result { case .success: break @@ -530,6 +571,7 @@ public class ApiVideoPlayerController: NSObject { } } } else { +// updatePlaybackProgress(currentTime: currentTime.seconds, duration: duration.seconds) self.analytics?.resume { result in switch result { case .success: break @@ -537,9 +579,57 @@ public class ApiVideoPlayerController: NSObject { } } } + try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback) + try? AVAudioSession.sharedInstance().setActive(true) + infoCenter.playbackState = .playing self.multicastDelegate.didPlay() } + func setupCommandCenter() { + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.isEnabled = true + commandCenter.pauseCommand.isEnabled = true + } + + private func updatePlaybackProgress() { + // Update the Now Playing Info with the playback progress + let infoCenter = MPNowPlayingInfoCenter.default() + var nowPlayingInfo = infoCenter.nowPlayingInfo ?? [:] + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = round(self.currentTime.seconds) + print("currentime second update: \(round(self.currentTime.seconds))") + infoCenter.nowPlayingInfo = nowPlayingInfo + } + + func remoteControlEventReceived(with event: UIEvent?) { + updatePlaybackProgress() + if let event = event { + switch event.subtype { + case .remoteControlPlay: + // Handle play remote control event + self.play() + case .remoteControlPause: + self.pause() + // Handle pause remote control event + case .remoteControlNextTrack: + self.seek(to: self.duration) + // Handle next track remote control event + case .remoteControlPreviousTrack: + + self.replay() + // Handle previous track remote control event + case .remoteControlStop: + // Handle stop remote control event + break + case .remoteControlTogglePlayPause: + // Handle toggle play/pause remote control event + break + default: + break + } + + } + } + private func doTimeControlStatus() { let status = self.avPlayer.timeControlStatus switch status { diff --git a/Sources/ApiVideoPlayer/Request/RequestsBuilder.swift b/Sources/ApiVideoPlayer/Request/RequestsBuilder.swift index 69a7381c..d52157d1 100644 --- a/Sources/ApiVideoPlayer/Request/RequestsBuilder.swift +++ b/Sources/ApiVideoPlayer/Request/RequestsBuilder.swift @@ -44,4 +44,25 @@ enum RequestsBuilder { } } } + + static func getThumbnailImage( + taskExecutor: TasksExecutorProtocol.Type, + url: URL, + completion: @escaping (Data) -> Void, + didError: @escaping (Error) -> Void + ) { + let request = buildUrlRequest(url: url) + let session = buildUrlSession() + taskExecutor.execute(session: session, request: request) { data, error in + if let data = data { + completion(data) + } else { + if let error = error { + didError(error) + } else { + didError(PlayerError.sessionTokenError("Request error, failed to get thumbnail")) + } + } + } + } } diff --git a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift index e4158f03..19d42c2c 100644 --- a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift +++ b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift @@ -238,6 +238,10 @@ public class ApiVideoPlayerView: UIView { self.playerController.isLooping = newValue } } + + public func remoteControlEventReceived(with event: UIEvent?) { + self.playerController.remoteControlEventReceived(with: event) + } } #else From 04e5cf97ff18b61952bd98f5f3ef65ebf965ea86 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Thu, 20 Apr 2023 16:21:08 +0200 Subject: [PATCH 02/55] feat() add class to handle 'MPNowPlayingInfoCenter' --- .../ApiVideoPlayerInformationNowPlaying.swift | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift new file mode 100644 index 00000000..73f7437c --- /dev/null +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -0,0 +1,70 @@ +import Foundation +import MediaPlayer + +protocol InformationNowPlaying { + func update(metadata: [String: Any]?) + func pause(currentTime: CMTime) + func play(currentTime: CMTime) + func overrideInformations(for key: String, value: Any) +} + +final class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { + private var infos = [String: Any]() + private let taskExecutor: TasksExecutorProtocol.Type + + init(taskExecutor: TasksExecutorProtocol.Type) { + self.taskExecutor = taskExecutor + } + + func update(metadata: [String: Any]?) { + infos[MPMediaItemPropertyTitle] = metadata?["title"] ?? "" + if let duration = metadata?["duration"] as? CMTime { + print("duration info center : \(duration)") + infos[MPMediaItemPropertyPlaybackDuration] = duration.seconds + } + if let currentTime = metadata?["currentTime"] as? CMTime { + infos[MPNowPlayingInfoPropertyElapsedPlaybackTime] = round(currentTime.seconds) + } + infos[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 + + if let imageStr = metadata?["thumbnailUrl"] as? String { + if let url = URL(string: imageStr) { + updateRemoteImage(url: url) + } + } + MPNowPlayingInfoCenter.default().nowPlayingInfo = infos + } + + func pause(currentTime: CMTime) { + infos[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 + self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: round(currentTime.seconds)) + } + + func play(currentTime: CMTime) { + infos[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 + self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: round(currentTime.seconds)) + } + + func overrideInformations(for key: String, value: Any) { + infos[key] = value + print("infos : \(infos.values)") + MPNowPlayingInfoCenter.default().nowPlayingInfo = infos + } + + private func getArtwork(image: UIImage) -> MPMediaItemArtwork { + return MPMediaItemArtwork(boundsSize: image.size) { _ in image } + } + + private func updateRemoteImage(url: URL) { + RequestsBuilder.getThumbnailImage(taskExecutor: self.taskExecutor, url: url, completion: { data in + guard let uiImage = UIImage(data: data) else { + return + } + let artwork = self.getArtwork(image: uiImage) + self.overrideInformations(for: MPMediaItemPropertyArtwork, value: artwork) + }, didError: { _ in + + }) + } + +} From de5167d52bb57b3066928768e7ba6336546221c5 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Thu, 20 Apr 2023 16:21:43 +0200 Subject: [PATCH 03/55] fix() remove old implementation of 'MPNowPlayingInfoCenter' and use new custom class --- .../ApiVideoPlayerController.swift | 50 ++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index d29d09b7..fcd3b5c7 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -17,6 +17,7 @@ public class ApiVideoPlayerController: NSObject { private let multicastDelegate = ApiVideoPlayerControllerMulticastDelegate() private var playerItemFactory: ApiVideoPlayerItemFactory? private var storedSpeedRate: Float = 1.0 + private var infoNowPlaying: ApiVideoPlayerInformationNowPlaying? #if !os(macOS) /// Initializes a player controller. @@ -55,6 +56,7 @@ public class ApiVideoPlayerController: NSObject { ) { multicastDelegate.addDelegates(delegates) self.taskExecutor = taskExecutor + self.infoNowPlaying = ApiVideoPlayerInformationNowPlaying(taskExecutor: taskExecutor) super.init() defer { @@ -227,6 +229,10 @@ public class ApiVideoPlayerController: NSObject { case let .failure(error): print("analytics error on seek event: \(error)") } } + self.infoNowPlaying?.overrideInformations( + for: MPNowPlayingInfoPropertyElapsedPlaybackTime, + value: self.currentTime.seconds + ) completion(completed) } } @@ -541,28 +547,15 @@ public class ApiVideoPlayerController: NSObject { let infoCenter = MPNowPlayingInfoCenter.default() if self.isFirstPlay { self.isFirstPlay = false - // TODO: add MPNowPlayingInfoCenter + var metadata = [ + "duration": self.duration, + "currentTime": self.currentTime + ] as [String: Any] - // Configure Now Playing Info - let infoCenter = MPNowPlayingInfoCenter.default() - - loadThumabnail(completion: { image in - let artwork = MPMediaItemArtwork(boundsSize: image!.size, requestHandler: { _ -> UIImage in - return image ?? UIImage() - }) - - // Set the title of the video as the Now Playing Info title - let nowPlayingInfo: [String: Any] = [ - MPMediaItemPropertyArtwork: artwork, - MPMediaItemPropertyPlaybackDuration: self.duration.seconds, - MPNowPlayingInfoPropertyElapsedPlaybackTime: self.currentTime.seconds - ] - print("currentime second first play: \(self.currentTime.seconds)") -// nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.currentTime.seconds -// nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = self.duration.seconds - infoCenter.nowPlayingInfo = nowPlayingInfo - }) - // setupCommandCenter() + if let thumbnail = self.videoOptions?.thumbnailUrl { + metadata["thumbnailUrl"] = thumbnail + } + self.infoNowPlaying?.update(metadata: metadata) self.analytics?.play { result in switch result { @@ -571,7 +564,6 @@ public class ApiVideoPlayerController: NSObject { } } } else { -// updatePlaybackProgress(currentTime: currentTime.seconds, duration: duration.seconds) self.analytics?.resume { result in switch result { case .success: break @@ -591,30 +583,21 @@ public class ApiVideoPlayerController: NSObject { commandCenter.pauseCommand.isEnabled = true } - private func updatePlaybackProgress() { - // Update the Now Playing Info with the playback progress - let infoCenter = MPNowPlayingInfoCenter.default() - var nowPlayingInfo = infoCenter.nowPlayingInfo ?? [:] - nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = round(self.currentTime.seconds) - print("currentime second update: \(round(self.currentTime.seconds))") - infoCenter.nowPlayingInfo = nowPlayingInfo - } - func remoteControlEventReceived(with event: UIEvent?) { - updatePlaybackProgress() if let event = event { switch event.subtype { case .remoteControlPlay: // Handle play remote control event + self.infoNowPlaying?.play(currentTime: self.currentTime) self.play() case .remoteControlPause: + self.infoNowPlaying?.pause(currentTime: self.currentTime) self.pause() // Handle pause remote control event case .remoteControlNextTrack: self.seek(to: self.duration) // Handle next track remote control event case .remoteControlPreviousTrack: - self.replay() // Handle previous track remote control event case .remoteControlStop: @@ -626,7 +609,6 @@ public class ApiVideoPlayerController: NSObject { default: break } - } } From f80ad9d050116367a8ded0199282275944ee4018 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Fri, 21 Apr 2023 11:27:17 +0200 Subject: [PATCH 04/55] fix() remove explicit return --- Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift b/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift index b403acef..8781e019 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift +++ b/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift @@ -186,7 +186,7 @@ class PlayerViewController: UIViewController { } override var canBecomeFirstResponder: Bool { - return true + true } override func remoteControlReceived(with event: UIEvent?) { From 10a1090ee10c323309654b7e07947064e515e200 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Fri, 21 Apr 2023 11:27:56 +0200 Subject: [PATCH 05/55] fix() build error for macos, --- .../ApiVideoPlayerController.swift | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index fcd3b5c7..b63641e3 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -17,9 +17,10 @@ public class ApiVideoPlayerController: NSObject { private let multicastDelegate = ApiVideoPlayerControllerMulticastDelegate() private var playerItemFactory: ApiVideoPlayerItemFactory? private var storedSpeedRate: Float = 1.0 - private var infoNowPlaying: ApiVideoPlayerInformationNowPlaying? #if !os(macOS) + private var infoNowPlaying: ApiVideoPlayerInformationNowPlaying? + /// Initializes a player controller. /// - Parameters: /// - videoOptions: The video to play. @@ -37,6 +38,7 @@ public class ApiVideoPlayerController: NSObject { delegates: delegates, autoplay: autoplay ) + self.infoNowPlaying = ApiVideoPlayerInformationNowPlaying(taskExecutor: taskExecutor) playerLayer.player = self.avPlayer } #endif @@ -229,10 +231,12 @@ public class ApiVideoPlayerController: NSObject { case let .failure(error): print("analytics error on seek event: \(error)") } } + #if !os(macOS) self.infoNowPlaying?.overrideInformations( for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: self.currentTime.seconds ) + #endif completion(completed) } } @@ -523,30 +527,14 @@ public class ApiVideoPlayerController: NSObject { self.multicastDelegate.didPause() } - private func loadThumabnail(completion: @escaping (UIImage?) -> Void) { - if let thumbnailUrl = self.videoOptions?.thumbnailUrl { - if let url = URL(string: thumbnailUrl) { - RequestsBuilder.getThumbnailImage(taskExecutor: self.taskExecutor, url: url, completion: { data in - completion(UIImage(data: data)) - }, didError: { error in - self.multicastDelegate.didError(error) - }) - } else { - self.multicastDelegate.didError(PlayerError.urlError("Error on URL with this thumbnail string url")) - } - } else { - self.multicastDelegate.didError(PlayerError.urlError("can not get thumbnail from this url")) - } - } - private func doPlayAction() { if self.isSeeking { self.isSeeking = false return } - let infoCenter = MPNowPlayingInfoCenter.default() if self.isFirstPlay { self.isFirstPlay = false + #if !os(macOS) var metadata = [ "duration": self.duration, "currentTime": self.currentTime @@ -555,8 +543,13 @@ public class ApiVideoPlayerController: NSObject { if let thumbnail = self.videoOptions?.thumbnailUrl { metadata["thumbnailUrl"] = thumbnail } - self.infoNowPlaying?.update(metadata: metadata) + if let videoType = self.videoOptions?.videoType { + let isLive = videoType == .live ? true : false + metadata["isLive"] = isLive + } + self.infoNowPlaying?.update(metadata: metadata) + #endif self.analytics?.play { result in switch result { case .success: break @@ -571,9 +564,10 @@ public class ApiVideoPlayerController: NSObject { } } } + #if !os(macOS) try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback) try? AVAudioSession.sharedInstance().setActive(true) - infoCenter.playbackState = .playing + #endif self.multicastDelegate.didPlay() } @@ -583,6 +577,7 @@ public class ApiVideoPlayerController: NSObject { commandCenter.pauseCommand.isEnabled = true } + #if !os(macOS) func remoteControlEventReceived(with event: UIEvent?) { if let event = event { switch event.subtype { @@ -590,27 +585,34 @@ public class ApiVideoPlayerController: NSObject { // Handle play remote control event self.infoNowPlaying?.play(currentTime: self.currentTime) self.play() + case .remoteControlPause: - self.infoNowPlaying?.pause(currentTime: self.currentTime) self.pause() + self.infoNowPlaying?.pause(currentTime: self.currentTime) + // Handle pause remote control event case .remoteControlNextTrack: self.seek(to: self.duration) + // Handle next track remote control event case .remoteControlPreviousTrack: self.replay() + // Handle previous track remote control event case .remoteControlStop: // Handle stop remote control event break + case .remoteControlTogglePlayPause: // Handle toggle play/pause remote control event break + default: break } } } + #endif private func doTimeControlStatus() { let status = self.avPlayer.timeControlStatus From 1b15f1640c0fa2e97505c04695ce5de97268d2f3 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Fri, 21 Apr 2023 11:28:34 +0200 Subject: [PATCH 06/55] feat() add livestream information, and playback state --- .../ApiVideoPlayerInformationNowPlaying.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift index 73f7437c..d9efae48 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -8,7 +8,8 @@ protocol InformationNowPlaying { func overrideInformations(for key: String, value: Any) } -final class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { +#if !os(macOS) +class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { private var infos = [String: Any]() private let taskExecutor: TasksExecutorProtocol.Type @@ -25,6 +26,9 @@ final class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { if let currentTime = metadata?["currentTime"] as? CMTime { infos[MPNowPlayingInfoPropertyElapsedPlaybackTime] = round(currentTime.seconds) } + if let isLive = metadata?["isLive"] as? Bool { + infos[MPNowPlayingInfoPropertyIsLiveStream] = isLive + } infos[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 if let imageStr = metadata?["thumbnailUrl"] as? String { @@ -37,12 +41,14 @@ final class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { func pause(currentTime: CMTime) { infos[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 - self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: round(currentTime.seconds)) + MPNowPlayingInfoCenter.default().playbackState = .paused + self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: currentTime.seconds) } func play(currentTime: CMTime) { infos[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 - self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: round(currentTime.seconds)) + MPNowPlayingInfoCenter.default().playbackState = .playing + self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: currentTime.seconds) } func overrideInformations(for key: String, value: Any) { @@ -68,3 +74,4 @@ final class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { } } +#endif From 246458811cc1f8ae7dedb799027061875db180d4 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Fri, 21 Apr 2023 15:52:59 +0200 Subject: [PATCH 07/55] feat() add remoteControl to SwiftUI --- .../PlayerSwiftUI.xcodeproj/project.pbxproj | 4 ++++ Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist | 10 ++++++++++ .../SwiftUI/SwiftUIPlayerViewController.swift | 13 +++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist diff --git a/Examples/PlayerSwiftUI/PlayerSwiftUI.xcodeproj/project.pbxproj b/Examples/PlayerSwiftUI/PlayerSwiftUI.xcodeproj/project.pbxproj index c5b4a063..d284d58e 100644 --- a/Examples/PlayerSwiftUI/PlayerSwiftUI.xcodeproj/project.pbxproj +++ b/Examples/PlayerSwiftUI/PlayerSwiftUI.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ C734306628F453F900A82721 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C734306928F453F900A82721 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; C734307128F4540D00A82721 /* api.video-swift-player */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "api.video-swift-player"; path = ../..; sourceTree = ""; }; + C77D97A629F2CB7C00987B1F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -55,6 +56,7 @@ C734306128F453F900A82721 /* PlayerSwiftUI */ = { isa = PBXGroup; children = ( + C77D97A629F2CB7C00987B1F /* Info.plist */, C734306228F453F900A82721 /* PlayerSwiftUIApp.swift */, C734306428F453F900A82721 /* ContentView.swift */, C734306628F453F900A82721 /* Assets.xcassets */, @@ -285,6 +287,7 @@ DEVELOPMENT_TEAM = VY3VXRC7P4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PlayerSwiftUI/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -315,6 +318,7 @@ DEVELOPMENT_TEAM = VY3VXRC7P4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PlayerSwiftUI/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist b/Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist new file mode 100644 index 00000000..f753731e --- /dev/null +++ b/Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist @@ -0,0 +1,10 @@ + + + + + UIBackgroundModes + + audio + + + diff --git a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift index b41c7e82..808ac0a9 100644 --- a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift +++ b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift @@ -18,6 +18,7 @@ public class SwiftUIPlayerViewController: UIViewController { ) self.events = events super.init(nibName: nil, bundle: nil) + UIApplication.shared.beginReceivingRemoteControlEvents() playerView.addDelegate(self) } @@ -47,6 +48,18 @@ public class SwiftUIPlayerViewController: UIViewController { super.viewDidDisappear(animated) } + override public var canBecomeFirstResponder: Bool { + true + } + + override public func remoteControlReceived(with event: UIEvent?) { + if let event = event { + if event.type == .remoteControl { + self.playerView.remoteControlEventReceived(with: event) + } + } + } + public func play() { self.playerView.play() } From 28022d6fc4c94a877a4d6559806218dd28625b01 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Mon, 24 Apr 2023 15:18:33 +0200 Subject: [PATCH 08/55] chore() add doc for remote control --- README.md | 6 +- .../Adding video player remote control.md | 68 +++++++++++++++++++ .../Documentation.docc/Documentation.md | 1 + 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md diff --git a/README.md b/README.md index ffcdfdfb..812337b7 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,6 @@ a video Id to use this component and play a video from api.video. To get yours, Alternatively, you can find your video Id in the video details of your [dashboard](https://dashboard.api.video). -## Code sample ## Code sample @@ -180,6 +179,9 @@ override func viewDidAppear(_ animated: Bool) { } ``` +### Remote control +If you want to enable the remote control please check this [documentation](https://apivideo.github.io/api.video-swift-player/documentation/apivideoplayer/adding-video-player-remote-control) + # Sample application A demo application demonstrates how to use player. @@ -192,7 +194,7 @@ On the first run, you will have to set your video Id: # Documentation -* [API documentation](https://apivideo.github.io/api.video-swift-player/documentation/apivideoplayer/) +* [Player documentation](https://apivideo.github.io/api.video-swift-player/documentation/apivideoplayer/) * [api.video documentation](https://docs.api.video) # Dependencies diff --git a/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md b/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md new file mode 100644 index 00000000..72196aa0 --- /dev/null +++ b/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md @@ -0,0 +1,68 @@ +# Adding video player remote control + +On this article we will dive on how to display a remote controller on your lockscreen and notification center + +## Table of contents + +- [Adding permission](#adding-permission) + - [Swift Package Manager](#swift-package-manager) + - [Cocoapods](#cocoapods) +- [Receive events from remote-control](#receive-events-from-remote-control) + - [UIKit](#uikit) + - [SwiftUI](#swiftui) +- [Documentation](#documentation) +- [Dependencies](#dependencies) + +## Adding permission +First of all you have to add audio in background permission. +To do so follow the instruction : + 1. Go to your target application + 2. Select "Signing & Capabilities" + 3. Then click on " + Capability" button + 4. On this pop up select "Background Modes" + 5. finally select "Audio, AirPlay and Picture in Picture" + +## Receive events from remote-control +### UIKit +On your AppDelegate.swift, + - add in 'didFinishLaunchingWithOptions' function: +``` +UIApplication.shared.beginReceivingRemoteControlEvents() +``` +Then in your ViewController.swift + +override the 'remoteControlReceived' function and pass the events to 'remoteControlEventReceived' of ApiVideoSwiftPlayer + +``` +override func remoteControlReceived(with event: UIEvent?) { + if let event = event { + if event.type == .remoteControl { + self.playerView.remoteControlEventReceived(with: event) + } + } +} +``` + +### SwiftUI +For SwiftUI you don't have any to do, it's already handle by the library. + +That's it, now you can test it and play with the remote control. + + +## Documentation + +* [API documentation](https://apivideo.github.io/api.video-swift-player/documentation/apivideoplayer/) +* [api.video documentation](https://docs.api.video) + +## Dependencies + +We are using external library + +| Plugin | README | +|---------------------------------------------------------------------------------------|--------------------------------------------------------------------------------| +| [ApiVideoPlayerAnalytics](https://github.com/apivideo/api.video-ios-player-analytics) | [README.md](https://github.com/apivideo/api.video-ios-player-analytics#readme) | + +## FAQ + +If you have any questions, ask us here: [https://community.api.video](https://community.api.video) or +use [Issues](https://github.com/apivideo/api.video-ios-player-analytics/issues). diff --git a/Sources/ApiVideoPlayer/Documentation.docc/Documentation.md b/Sources/ApiVideoPlayer/Documentation.docc/Documentation.md index c9b48ea3..7e8a01f0 100644 --- a/Sources/ApiVideoPlayer/Documentation.docc/Documentation.md +++ b/Sources/ApiVideoPlayer/Documentation.docc/Documentation.md @@ -10,6 +10,7 @@ Swift library to add player for your videos stored at api.video. ### Essentials - +- ### UIKit - ``ApiVideoPlayerView`` From d2dba3986ceba568fc4c525c2908144323a68cf7 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Mon, 24 Apr 2023 15:40:58 +0200 Subject: [PATCH 09/55] chore(sample, doc) add comments, add 'endReceivingRemoteControlEvents' on sample app closed --- Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift | 5 +++++ .../Adding video player remote control.md | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift b/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift index 8e827530..c8ebcb32 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift +++ b/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift @@ -7,6 +7,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Override point for customization after application launch. + + // Allow application to receive events from remote control UIApplication.shared.beginReceivingRemoteControlEvents() return true } @@ -28,5 +30,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // If any sessions were discarded while the application was not running, this will be called shortly after // application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + + // Stop receiving remote control events + UIApplication.shared.endReceivingRemoteControlEvents() } } diff --git a/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md b/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md index 72196aa0..92fd5e3b 100644 --- a/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md +++ b/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md @@ -27,7 +27,13 @@ To do so follow the instruction : On your AppDelegate.swift, - add in 'didFinishLaunchingWithOptions' function: ``` +// Start receiving events UIApplication.shared.beginReceivingRemoteControlEvents() +``` + - add in 'didDiscardSceneSessions' function: +``` +// Stop receiving events +UIApplication.shared.endReceivingRemoteControlEvents() ``` Then in your ViewController.swift From d2fe0e66a35ec116d4b0125633008bb862cbd91a Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Mon, 24 Apr 2023 16:49:08 +0200 Subject: [PATCH 10/55] fix() move macos restrictions only for thumbnail uiimage, use avplayer 'currentrate', getThumbnail func send a UIImage instead of data --- .../ApiVideoPlayerController.swift | 10 ++---- .../ApiVideoPlayerInformationNowPlaying.swift | 35 +++++++++---------- .../Request/RequestsBuilder.swift | 16 ++++++--- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index b63641e3..c2d49efe 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -17,10 +17,9 @@ public class ApiVideoPlayerController: NSObject { private let multicastDelegate = ApiVideoPlayerControllerMulticastDelegate() private var playerItemFactory: ApiVideoPlayerItemFactory? private var storedSpeedRate: Float = 1.0 - - #if !os(macOS) private var infoNowPlaying: ApiVideoPlayerInformationNowPlaying? + #if !os(macOS) /// Initializes a player controller. /// - Parameters: /// - videoOptions: The video to play. @@ -38,7 +37,6 @@ public class ApiVideoPlayerController: NSObject { delegates: delegates, autoplay: autoplay ) - self.infoNowPlaying = ApiVideoPlayerInformationNowPlaying(taskExecutor: taskExecutor) playerLayer.player = self.avPlayer } #endif @@ -231,12 +229,10 @@ public class ApiVideoPlayerController: NSObject { case let .failure(error): print("analytics error on seek event: \(error)") } } - #if !os(macOS) self.infoNowPlaying?.overrideInformations( for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: self.currentTime.seconds ) - #endif completion(completed) } } @@ -583,12 +579,12 @@ public class ApiVideoPlayerController: NSObject { switch event.subtype { case .remoteControlPlay: // Handle play remote control event - self.infoNowPlaying?.play(currentTime: self.currentTime) self.play() + self.infoNowPlaying?.play(currentTime: self.currentTime, currentRate: self.avPlayer.rate) case .remoteControlPause: self.pause() - self.infoNowPlaying?.pause(currentTime: self.currentTime) + self.infoNowPlaying?.pause(currentTime: self.currentTime, currentRate: self.avPlayer.rate) // Handle pause remote control event case .remoteControlNextTrack: diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift index d9efae48..0ca81b89 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -3,12 +3,11 @@ import MediaPlayer protocol InformationNowPlaying { func update(metadata: [String: Any]?) - func pause(currentTime: CMTime) - func play(currentTime: CMTime) + func pause(currentTime: CMTime, currentRate: Float) + func play(currentTime: CMTime, currentRate: Float) func overrideInformations(for key: String, value: Any) } -#if !os(macOS) class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { private var infos = [String: Any]() private let taskExecutor: TasksExecutorProtocol.Type @@ -20,58 +19,56 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { func update(metadata: [String: Any]?) { infos[MPMediaItemPropertyTitle] = metadata?["title"] ?? "" if let duration = metadata?["duration"] as? CMTime { - print("duration info center : \(duration)") infos[MPMediaItemPropertyPlaybackDuration] = duration.seconds } if let currentTime = metadata?["currentTime"] as? CMTime { - infos[MPNowPlayingInfoPropertyElapsedPlaybackTime] = round(currentTime.seconds) + infos[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime.seconds } if let isLive = metadata?["isLive"] as? Bool { infos[MPNowPlayingInfoPropertyIsLiveStream] = isLive } infos[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 + #if !os(macOS) if let imageStr = metadata?["thumbnailUrl"] as? String { if let url = URL(string: imageStr) { updateRemoteImage(url: url) } } + #endif + MPNowPlayingInfoCenter.default().nowPlayingInfo = infos } - func pause(currentTime: CMTime) { - infos[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 + func pause(currentTime: CMTime, currentRate: Float) { + infos[MPNowPlayingInfoPropertyPlaybackRate] = currentRate MPNowPlayingInfoCenter.default().playbackState = .paused self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: currentTime.seconds) } - func play(currentTime: CMTime) { - infos[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 + func play(currentTime: CMTime, currentRate: Float) { + infos[MPNowPlayingInfoPropertyPlaybackRate] = currentRate MPNowPlayingInfoCenter.default().playbackState = .playing self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: currentTime.seconds) } func overrideInformations(for key: String, value: Any) { infos[key] = value - print("infos : \(infos.values)") MPNowPlayingInfoCenter.default().nowPlayingInfo = infos } + #if !os(macOS) private func getArtwork(image: UIImage) -> MPMediaItemArtwork { return MPMediaItemArtwork(boundsSize: image.size) { _ in image } } private func updateRemoteImage(url: URL) { - RequestsBuilder.getThumbnailImage(taskExecutor: self.taskExecutor, url: url, completion: { data in - guard let uiImage = UIImage(data: data) else { - return - } - let artwork = self.getArtwork(image: uiImage) + RequestsBuilder.getThumbnail(taskExecutor: self.taskExecutor, url: url, completion: { image in + let artwork = self.getArtwork(image: image) self.overrideInformations(for: MPMediaItemPropertyArtwork, value: artwork) - }, didError: { _ in - + }, didError: { error in + print("Error on artwork : \(error.localizedDescription)") }) } - + #endif } -#endif diff --git a/Sources/ApiVideoPlayer/Request/RequestsBuilder.swift b/Sources/ApiVideoPlayer/Request/RequestsBuilder.swift index d52157d1..db8eae71 100644 --- a/Sources/ApiVideoPlayer/Request/RequestsBuilder.swift +++ b/Sources/ApiVideoPlayer/Request/RequestsBuilder.swift @@ -1,5 +1,7 @@ import Foundation - +#if !os(macOS) +import UIKit +#endif /// Static methods to build the network requests. enum RequestsBuilder { private static func setContentType(request: inout URLRequest) { @@ -45,17 +47,22 @@ enum RequestsBuilder { } } - static func getThumbnailImage( + #if !os(macOS) + static func getThumbnail( taskExecutor: TasksExecutorProtocol.Type, url: URL, - completion: @escaping (Data) -> Void, + completion: @escaping (UIImage) -> Void, didError: @escaping (Error) -> Void ) { let request = buildUrlRequest(url: url) let session = buildUrlSession() taskExecutor.execute(session: session, request: request) { data, error in if let data = data { - completion(data) + if let uiImage = UIImage(data: data) { + completion(uiImage) + } else { + didError(PlayerError.videoError("Unable to create image from data")) + } } else { if let error = error { didError(error) @@ -65,4 +72,5 @@ enum RequestsBuilder { } } } + #endif } From 450363ceddfdae04f9db27988759ee2cd46f2f10 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Mon, 24 Apr 2023 16:52:13 +0200 Subject: [PATCH 11/55] fix() only add title to remote control if it's not nil --- .../ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift index 0ca81b89..c2d23b54 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -17,7 +17,9 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { } func update(metadata: [String: Any]?) { - infos[MPMediaItemPropertyTitle] = metadata?["title"] ?? "" + if let title = metadata?["title"] { + infos[MPMediaItemPropertyTitle] = title + } if let duration = metadata?["duration"] as? CMTime { infos[MPMediaItemPropertyPlaybackDuration] = duration.seconds } From a64b3878135889d41edf9755c09047968e3a9aa3 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Mon, 24 Apr 2023 16:55:17 +0200 Subject: [PATCH 12/55] chore() rename func to 'updateRemoteArtwork' --- .../ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift index c2d23b54..8b45ab91 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -34,7 +34,7 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { #if !os(macOS) if let imageStr = metadata?["thumbnailUrl"] as? String { if let url = URL(string: imageStr) { - updateRemoteImage(url: url) + updateRemoteArtwork(url: url) } } #endif @@ -64,7 +64,7 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { return MPMediaItemArtwork(boundsSize: image.size) { _ in image } } - private func updateRemoteImage(url: URL) { + private func updateRemoteArtwork(url: URL) { RequestsBuilder.getThumbnail(taskExecutor: self.taskExecutor, url: url, completion: { image in let artwork = self.getArtwork(image: image) self.overrideInformations(for: MPMediaItemPropertyArtwork, value: artwork) From a45d511a6bac53395c25084bf930fa644e1c7da1 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Mon, 24 Apr 2023 18:54:47 +0200 Subject: [PATCH 13/55] fix() update remote control on player tap --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index c2d49efe..7ffa2640 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -283,6 +283,7 @@ public class ApiVideoPlayerController: NSObject { /// Gets and sets the video options. public var videoOptions: VideoOptions? { didSet { + self.isFirstPlay = true guard let videoOptions = videoOptions else { resetPlayer(with: nil) return @@ -520,6 +521,7 @@ public class ApiVideoPlayerController: NSObject { case let .failure(error): print("analytics error on pause event: \(error)") } } + self.infoNowPlaying?.pause(currentTime: self.currentTime, currentRate: self.avPlayer.rate) self.multicastDelegate.didPause() } @@ -559,6 +561,7 @@ public class ApiVideoPlayerController: NSObject { case let .failure(error): print("analytics error on resume event: \(error)") } } + self.infoNowPlaying?.play(currentTime: self.currentTime, currentRate: self.avPlayer.rate) } #if !os(macOS) try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback) From 0abc44cb7b4e862b0c1f7289fc7824c6218a8ebc Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Mon, 24 Apr 2023 18:56:17 +0200 Subject: [PATCH 14/55] chore() add method to clear remote control, add description in doc --- .../ApiVideoPlayerController.swift | 11 +++++++++ .../ApiVideoPlayerInformationNowPlaying.swift | 5 ++++ .../Adding video player remote control.md | 24 ++++++++++++++++++- .../Views/ApiVideoPlayerView.swift | 12 ++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 7ffa2640..efe80c56 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -110,6 +110,17 @@ public class ApiVideoPlayerController: NSObject { multicastDelegate.removeDelegates(delegates) } + /// Allow video interaction with the remote control on lockscreen and notification center + func allowRemoteControl() { + self.infoNowPlaying = ApiVideoPlayerInformationNowPlaying(taskExecutor: taskExecutor) + } + + /// remove remote control from lockscreen and notification center + func removeRemoteControl() { + self.pause() + self.infoNowPlaying?.clearMPNowPlayingInfoCenter() + } + private func resetPlayer(with playerItem: AVPlayerItem? = nil) { if let currentItem = avPlayer.currentItem { currentItem.removeObserver(self, forKeyPath: "status", context: nil) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift index 8b45ab91..4b2e0191 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -6,6 +6,7 @@ protocol InformationNowPlaying { func pause(currentTime: CMTime, currentRate: Float) func play(currentTime: CMTime, currentRate: Float) func overrideInformations(for key: String, value: Any) + func clearMPNowPlayingInfoCenter() } class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { @@ -59,6 +60,10 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { MPNowPlayingInfoCenter.default().nowPlayingInfo = infos } + func clearMPNowPlayingInfoCenter() { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + } + #if !os(macOS) private func getArtwork(image: UIImage) -> MPMediaItemArtwork { return MPMediaItemArtwork(boundsSize: image.size) { _ in image } diff --git a/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md b/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md index 92fd5e3b..257777ca 100644 --- a/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md +++ b/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md @@ -37,7 +37,16 @@ UIApplication.shared.endReceivingRemoteControlEvents() ``` Then in your ViewController.swift -override the 'remoteControlReceived' function and pass the events to 'remoteControlEventReceived' of ApiVideoSwiftPlayer +After you have instanciated the player, call the method 'allowRemoteControl' to send data to the remote control + +``` +override func viewDidLoad() { +... +self.playerView.allowRemoteControl() +... +``` + +Now you need to override the 'remoteControlReceived' function and pass the events to 'remoteControlEventReceived' of ApiVideoSwiftPlayer ``` override func remoteControlReceived(with event: UIEvent?) { @@ -52,6 +61,19 @@ override func remoteControlReceived(with event: UIEvent?) { ### SwiftUI For SwiftUI you don't have any to do, it's already handle by the library. +## Remove remote control +Don't forget to remove it when player is stopped or destroyed. +for exemple on didEnd event + +``` +public func didEnd() { +... + self.playerView.removeRemoteControl() +... + +} +``` + That's it, now you can test it and play with the remote control. diff --git a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift index 19d42c2c..32e59a58 100644 --- a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift +++ b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift @@ -239,9 +239,21 @@ public class ApiVideoPlayerView: UIView { } } + /// Pass events received from remote control, to handle actions. + /// - Parameter event: event of type remoteControl public func remoteControlEventReceived(with event: UIEvent?) { self.playerController.remoteControlEventReceived(with: event) } + + /// Allow video interaction with the remote control on lockscreen and notification center + public func allowRemoteControl() { + self.playerController.allowRemoteControl() + } + + /// Remove remote control from lockscreen and notification center + public func removeRemoteControl() { + self.playerController.removeRemoteControl() + } } #else From ac75d74438193b179ecf7e1290338fc626d3bec8 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Mon, 24 Apr 2023 18:57:38 +0200 Subject: [PATCH 15/55] fix(sample) remove unnecessary first responder --- .../PlayerUIKit/PlayerUIkit/PlayerViewController.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift b/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift index 8781e019..b490eb23 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift +++ b/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift @@ -125,10 +125,9 @@ class PlayerViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.addSubview(self.scrollView) - self.becomeFirstResponder() - self.scrollView.addSubview(self.playerView) self.playerView.addDelegate(self) + self.playerView.allowRemoteControl() let doubleTap = UITapGestureRecognizer(target: self, action: #selector(self.handleDoubleTap(_:))) doubleTap.numberOfTapsRequired = 2 self.playerView.addGestureRecognizer(doubleTap) @@ -185,10 +184,6 @@ class PlayerViewController: UIViewController { self.playerView.viewController = self } - override var canBecomeFirstResponder: Bool { - true - } - override func remoteControlReceived(with event: UIEvent?) { if let event = event { if event.type == .remoteControl { From 86ea935e1553384d9d1da7a3a7755d05913a5d60 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Mon, 24 Apr 2023 18:58:09 +0200 Subject: [PATCH 16/55] chore(sample) illustrate removeRemoteControl --- Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift b/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift index b490eb23..c59b90eb 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift +++ b/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift @@ -339,6 +339,7 @@ extension PlayerViewController: ApiVideoPlayerControllerPlayerDelegate { } public func didEnd() { + self.playerView.removeRemoteControl() print("app didEnd") } From a599e26ff8ecf94e052c835464ec05f3fe9ea9c4 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Mon, 24 Apr 2023 19:37:55 +0200 Subject: [PATCH 17/55] fix() add NowPlayingData data struct, replace update method by using getter setter --- .../ApiVideoPlayerController.swift | 20 +++----- .../ApiVideoPlayerInformationNowPlaying.swift | 46 +++++++++---------- .../Models/NowPlayingData.swift | 10 ++++ 3 files changed, 40 insertions(+), 36 deletions(-) create mode 100644 Sources/ApiVideoPlayer/Models/NowPlayingData.swift diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index efe80c56..2eaf2bbb 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -544,20 +544,14 @@ public class ApiVideoPlayerController: NSObject { if self.isFirstPlay { self.isFirstPlay = false #if !os(macOS) - var metadata = [ - "duration": self.duration, - "currentTime": self.currentTime - ] as [String: Any] + let isLive = self.videoOptions?.videoType == .live ? true : false - if let thumbnail = self.videoOptions?.thumbnailUrl { - metadata["thumbnailUrl"] = thumbnail - } - if let videoType = self.videoOptions?.videoType { - let isLive = videoType == .live ? true : false - metadata["isLive"] = isLive - } - - self.infoNowPlaying?.update(metadata: metadata) + self.infoNowPlaying?.nowPlayingData = NowPlayingData( + duration: self.duration, + currentTime: self.currentTime, + thumbnailUrl: self.videoOptions?.thumbnailUrl, + isLive: isLive + ) #endif self.analytics?.play { result in switch result { diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift index 4b2e0191..527df37f 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -2,7 +2,7 @@ import Foundation import MediaPlayer protocol InformationNowPlaying { - func update(metadata: [String: Any]?) + var nowPlayingData: NowPlayingData? { get set } func pause(currentTime: CMTime, currentRate: Float) func play(currentTime: CMTime, currentRate: Float) func overrideInformations(for key: String, value: Any) @@ -17,30 +17,30 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { self.taskExecutor = taskExecutor } - func update(metadata: [String: Any]?) { - if let title = metadata?["title"] { - infos[MPMediaItemPropertyTitle] = title - } - if let duration = metadata?["duration"] as? CMTime { - infos[MPMediaItemPropertyPlaybackDuration] = duration.seconds - } - if let currentTime = metadata?["currentTime"] as? CMTime { - infos[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime.seconds - } - if let isLive = metadata?["isLive"] as? Bool { - infos[MPNowPlayingInfoPropertyIsLiveStream] = isLive - } - infos[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 - - #if !os(macOS) - if let imageStr = metadata?["thumbnailUrl"] as? String { - if let url = URL(string: imageStr) { - updateRemoteArtwork(url: url) + var nowPlayingData: NowPlayingData? { + didSet { + if let title = nowPlayingData?.title { + infos[MPMediaItemPropertyTitle] = title + } + if let duration = nowPlayingData?.duration { + infos[MPMediaItemPropertyPlaybackDuration] = duration.seconds } + if let currentTime = nowPlayingData?.currentTime { + infos[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime.seconds + } + if let live = nowPlayingData?.isLive { + infos[MPNowPlayingInfoPropertyIsLiveStream] = live + } + infos[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 + #if !os(macOS) + if let thumb = nowPlayingData?.thumbnailUrl { + if let url = URL(string: thumb) { + updateRemoteArtwork(url: url) + } + } + #endif + MPNowPlayingInfoCenter.default().nowPlayingInfo = infos } - #endif - - MPNowPlayingInfoCenter.default().nowPlayingInfo = infos } func pause(currentTime: CMTime, currentRate: Float) { diff --git a/Sources/ApiVideoPlayer/Models/NowPlayingData.swift b/Sources/ApiVideoPlayer/Models/NowPlayingData.swift new file mode 100644 index 00000000..c50e3d09 --- /dev/null +++ b/Sources/ApiVideoPlayer/Models/NowPlayingData.swift @@ -0,0 +1,10 @@ +import CoreMedia +import Foundation + +struct NowPlayingData { + var duration: CMTime? + var currentTime: CMTime? + var thumbnailUrl: String? + var isLive: Bool? + var title: String? +} From ce04e8c4c319497f3b45ae984de9c19276cbc273 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 25 Apr 2023 12:06:35 +0200 Subject: [PATCH 18/55] fix() replace var to let and remove optional --- .../ApiVideoPlayer/Models/NowPlayingData.swift | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Sources/ApiVideoPlayer/Models/NowPlayingData.swift b/Sources/ApiVideoPlayer/Models/NowPlayingData.swift index c50e3d09..4994c6de 100644 --- a/Sources/ApiVideoPlayer/Models/NowPlayingData.swift +++ b/Sources/ApiVideoPlayer/Models/NowPlayingData.swift @@ -2,9 +2,17 @@ import CoreMedia import Foundation struct NowPlayingData { - var duration: CMTime? - var currentTime: CMTime? - var thumbnailUrl: String? - var isLive: Bool? - var title: String? + let duration: CMTime + let currentTime: CMTime + let isLive: Bool + let thumbnailUrl: String? + let title: String? + + init(duration: CMTime, currentTime: CMTime, isLive: Bool, thumbnailUrl: String?, title: String? = nil) { + self.duration = duration + self.currentTime = currentTime + self.isLive = isLive + self.thumbnailUrl = thumbnailUrl + self.title = title + } } From a3de546ba1b00f3141e29abd42266a8486141935 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 25 Apr 2023 12:06:59 +0200 Subject: [PATCH 19/55] fix(doc) rewording --- .../Documentation.docc/Adding video player remote control.md | 5 +++-- Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md b/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md index 257777ca..59ad6b99 100644 --- a/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md +++ b/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md @@ -62,9 +62,10 @@ override func remoteControlReceived(with event: UIEvent?) { For SwiftUI you don't have any to do, it's already handle by the library. ## Remove remote control -Don't forget to remove it when player is stopped or destroyed. -for exemple on didEnd event +You can use the 'removeRemoteControl' function to take out the remote control. +For instance, when a video reaches its end, you have the option to remove it. +It's important to note that using this method will pause the video, and if you play it again, the remote control will be displayed one more. ``` public func didEnd() { ... diff --git a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift index 32e59a58..d9b4fbc2 100644 --- a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift +++ b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift @@ -250,7 +250,9 @@ public class ApiVideoPlayerView: UIView { self.playerController.allowRemoteControl() } - /// Remove remote control from lockscreen and notification center + /// Remove remote control from lockscreen and notification center. + /// This is not definitive, when play again a video the remote control will be displayed. + /// It's recommended to call this function when you're done playing the video. public func removeRemoteControl() { self.playerController.removeRemoteControl() } From 645a6ba8078f8e80959b350406b7662bc393a9f3 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 25 Apr 2023 12:07:32 +0200 Subject: [PATCH 20/55] chore() rework removeRemoteControl --- .../ApiVideoPlayerController.swift | 17 +++++++---------- .../ApiVideoPlayerInformationNowPlaying.swift | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 2eaf2bbb..3e914b87 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -118,6 +118,11 @@ public class ApiVideoPlayerController: NSObject { /// remove remote control from lockscreen and notification center func removeRemoteControl() { self.pause() + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch { + print("Error deactivating audio session: \(error.localizedDescription)") + } self.infoNowPlaying?.clearMPNowPlayingInfoCenter() } @@ -544,13 +549,11 @@ public class ApiVideoPlayerController: NSObject { if self.isFirstPlay { self.isFirstPlay = false #if !os(macOS) - let isLive = self.videoOptions?.videoType == .live ? true : false - self.infoNowPlaying?.nowPlayingData = NowPlayingData( duration: self.duration, currentTime: self.currentTime, - thumbnailUrl: self.videoOptions?.thumbnailUrl, - isLive: isLive + isLive: self.isLive, + thumbnailUrl: self.videoOptions?.thumbnailUrl ) #endif self.analytics?.play { result in @@ -575,12 +578,6 @@ public class ApiVideoPlayerController: NSObject { self.multicastDelegate.didPlay() } - func setupCommandCenter() { - let commandCenter = MPRemoteCommandCenter.shared() - commandCenter.playCommand.isEnabled = true - commandCenter.pauseCommand.isEnabled = true - } - #if !os(macOS) func remoteControlEventReceived(with event: UIEvent?) { if let event = event { diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift index 527df37f..27411c39 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -61,7 +61,7 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { } func clearMPNowPlayingInfoCenter() { - MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + MPNowPlayingInfoCenter.default().nowPlayingInfo = [:] } #if !os(macOS) From d8874180432bc15fc330a967a14f7294378e9b42 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 25 Apr 2023 12:11:52 +0200 Subject: [PATCH 21/55] chore() fix error macos build --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 3e914b87..c92f484a 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -118,11 +118,13 @@ public class ApiVideoPlayerController: NSObject { /// remove remote control from lockscreen and notification center func removeRemoteControl() { self.pause() + #if !os(macOS) do { try AVAudioSession.sharedInstance().setActive(false) } catch { print("Error deactivating audio session: \(error.localizedDescription)") } + #endif self.infoNowPlaying?.clearMPNowPlayingInfoCenter() } From c013474f49d629942b3a905ee0e604f28cecfabe Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 25 Apr 2023 16:48:50 +0200 Subject: [PATCH 22/55] fix() remove UIBackground audio --- Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist | 7 +------ Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift | 2 +- Examples/PlayerUIKit/PlayerUIkit/Info.plist | 4 ---- .../Adding video player remote control.md | 12 ------------ 4 files changed, 2 insertions(+), 23 deletions(-) diff --git a/Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist b/Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist index f753731e..0c67376e 100644 --- a/Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist +++ b/Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist @@ -1,10 +1,5 @@ - - UIBackgroundModes - - audio - - + diff --git a/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift b/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift index c8ebcb32..e2a66c75 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift +++ b/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift @@ -9,7 +9,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. // Allow application to receive events from remote control - UIApplication.shared.beginReceivingRemoteControlEvents() +// UIApplication.shared.beginReceivingRemoteControlEvents() return true } diff --git a/Examples/PlayerUIKit/PlayerUIkit/Info.plist b/Examples/PlayerUIKit/PlayerUIkit/Info.plist index 8e6b5a88..dd3c9afd 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/Info.plist +++ b/Examples/PlayerUIKit/PlayerUIkit/Info.plist @@ -21,9 +21,5 @@ - UIBackgroundModes - - audio - diff --git a/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md b/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md index 59ad6b99..733389ba 100644 --- a/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md +++ b/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md @@ -4,24 +4,12 @@ On this article we will dive on how to display a remote controller on your locks ## Table of contents -- [Adding permission](#adding-permission) - - [Swift Package Manager](#swift-package-manager) - - [Cocoapods](#cocoapods) - [Receive events from remote-control](#receive-events-from-remote-control) - [UIKit](#uikit) - [SwiftUI](#swiftui) - [Documentation](#documentation) - [Dependencies](#dependencies) -## Adding permission -First of all you have to add audio in background permission. -To do so follow the instruction : - 1. Go to your target application - 2. Select "Signing & Capabilities" - 3. Then click on " + Capability" button - 4. On this pop up select "Background Modes" - 5. finally select "Audio, AirPlay and Picture in Picture" - ## Receive events from remote-control ### UIKit On your AppDelegate.swift, From 3c752e77e4d4d88fd995742acd8a2ccd32bac874 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 25 Apr 2023 17:18:14 +0200 Subject: [PATCH 23/55] fix() declare title to nil --- Sources/ApiVideoPlayer/Models/NowPlayingData.swift | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Sources/ApiVideoPlayer/Models/NowPlayingData.swift b/Sources/ApiVideoPlayer/Models/NowPlayingData.swift index 4994c6de..4c0a15e0 100644 --- a/Sources/ApiVideoPlayer/Models/NowPlayingData.swift +++ b/Sources/ApiVideoPlayer/Models/NowPlayingData.swift @@ -6,13 +6,5 @@ struct NowPlayingData { let currentTime: CMTime let isLive: Bool let thumbnailUrl: String? - let title: String? - - init(duration: CMTime, currentTime: CMTime, isLive: Bool, thumbnailUrl: String?, title: String? = nil) { - self.duration = duration - self.currentTime = currentTime - self.isLive = isLive - self.thumbnailUrl = thumbnailUrl - self.title = title - } + let title: String? = nil } From 1b0b028c7de47b5afaec18a2dfff440eccf022df Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 25 Apr 2023 19:08:21 +0200 Subject: [PATCH 24/55] feat(swiftui) add get set to enable remote control --- .../ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift | 6 ++++++ .../SwiftUI/SwiftUIPlayerViewController.swift | 16 +++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift b/Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift index d672af3e..af087473 100644 --- a/Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift +++ b/Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift @@ -113,6 +113,12 @@ public struct ApiVideoPlayer: UIViewControllerRepresentable { } } + public var enableRemoteControl: Bool = false { + didSet { + self.playerViewController.enableRemoteControl = enableRemoteControl + } + } + } struct SwiftUIView_Previews: PreviewProvider { diff --git a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift index 808ac0a9..46f29f82 100644 --- a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift +++ b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift @@ -18,7 +18,6 @@ public class SwiftUIPlayerViewController: UIViewController { ) self.events = events super.init(nibName: nil, bundle: nil) - UIApplication.shared.beginReceivingRemoteControlEvents() playerView.addDelegate(self) } @@ -45,13 +44,10 @@ public class SwiftUIPlayerViewController: UIViewController { override public func viewDidDisappear(_ animated: Bool) { self.playerView.viewController = nil + UIApplication.shared.endReceivingRemoteControlEvents() super.viewDidDisappear(animated) } - override public var canBecomeFirstResponder: Bool { - true - } - override public func remoteControlReceived(with event: UIEvent?) { if let event = event { if event.type == .remoteControl { @@ -131,6 +127,16 @@ public class SwiftUIPlayerViewController: UIViewController { } } + public var enableRemoteControl: Bool = false { + didSet { + if enableRemoteControl { + UIApplication.shared.beginReceivingRemoteControlEvents() + } else { + UIApplication.shared.endReceivingRemoteControlEvents() + } + } + } + } extension SwiftUIPlayerViewController: ApiVideoPlayerControllerPlayerDelegate { From c5669180efd4dbe43840ba8e1155f4416b7e2bbd Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 25 Apr 2023 19:09:21 +0200 Subject: [PATCH 25/55] chore() remove not used api for remote control --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 8 +------- Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift | 5 ----- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index c92f484a..c79bdd37 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -110,11 +110,6 @@ public class ApiVideoPlayerController: NSObject { multicastDelegate.removeDelegates(delegates) } - /// Allow video interaction with the remote control on lockscreen and notification center - func allowRemoteControl() { - self.infoNowPlaying = ApiVideoPlayerInformationNowPlaying(taskExecutor: taskExecutor) - } - /// remove remote control from lockscreen and notification center func removeRemoteControl() { self.pause() @@ -557,6 +552,7 @@ public class ApiVideoPlayerController: NSObject { isLive: self.isLive, thumbnailUrl: self.videoOptions?.thumbnailUrl ) + #endif self.analytics?.play { result in switch result { @@ -587,11 +583,9 @@ public class ApiVideoPlayerController: NSObject { case .remoteControlPlay: // Handle play remote control event self.play() - self.infoNowPlaying?.play(currentTime: self.currentTime, currentRate: self.avPlayer.rate) case .remoteControlPause: self.pause() - self.infoNowPlaying?.pause(currentTime: self.currentTime, currentRate: self.avPlayer.rate) // Handle pause remote control event case .remoteControlNextTrack: diff --git a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift index d9b4fbc2..efcd7634 100644 --- a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift +++ b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift @@ -245,11 +245,6 @@ public class ApiVideoPlayerView: UIView { self.playerController.remoteControlEventReceived(with: event) } - /// Allow video interaction with the remote control on lockscreen and notification center - public func allowRemoteControl() { - self.playerController.allowRemoteControl() - } - /// Remove remote control from lockscreen and notification center. /// This is not definitive, when play again a video the remote control will be displayed. /// It's recommended to call this function when you're done playing the video. From 62bd9de6b8f550f140ed089fe599b88742f90389 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Wed, 26 Apr 2023 15:54:33 +0200 Subject: [PATCH 26/55] fix() replace default remote control by 'MPRemoteCommandCenter', remove events from default --- .../ApiVideoPlayerController.swift | 83 +++++++++---------- .../SwiftUI/SwiftUIPlayerViewController.swift | 8 -- .../Views/ApiVideoPlayerView.swift | 11 ++- 3 files changed, 48 insertions(+), 54 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index c79bdd37..b9486d08 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -17,7 +17,8 @@ public class ApiVideoPlayerController: NSObject { private let multicastDelegate = ApiVideoPlayerControllerMulticastDelegate() private var playerItemFactory: ApiVideoPlayerItemFactory? private var storedSpeedRate: Float = 1.0 - private var infoNowPlaying: ApiVideoPlayerInformationNowPlaying? + private var infoNowPlaying: ApiVideoPlayerInformationNowPlaying + private let rcc = MPRemoteCommandCenter.shared() #if !os(macOS) /// Initializes a player controller. @@ -62,7 +63,6 @@ public class ApiVideoPlayerController: NSObject { defer { self.videoOptions = videoOptions } - self.autoplay = autoplay self.avPlayer.addObserver( self, @@ -120,7 +120,7 @@ public class ApiVideoPlayerController: NSObject { print("Error deactivating audio session: \(error.localizedDescription)") } #endif - self.infoNowPlaying?.clearMPNowPlayingInfoCenter() + self.infoNowPlaying.clearMPNowPlayingInfoCenter() } private func resetPlayer(with playerItem: AVPlayerItem? = nil) { @@ -242,7 +242,7 @@ public class ApiVideoPlayerController: NSObject { case let .failure(error): print("analytics error on seek event: \(error)") } } - self.infoNowPlaying?.overrideInformations( + self.infoNowPlaying.overrideInformations( for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: self.currentTime.seconds ) @@ -436,6 +436,17 @@ public class ApiVideoPlayerController: NSObject { if isPlaying { avPlayer.rate = newRate } + + } + } + + public var enableRemoteControl: Bool = false { + didSet { + if enableRemoteControl { + self.setupRemoteControls() + } else { + UIApplication.shared.endReceivingRemoteControlEvents() + } } } @@ -488,6 +499,29 @@ public class ApiVideoPlayerController: NSObject { self.multicastDelegate.didEnd() } + private func setupRemoteControls() { + rcc.skipForwardCommand.preferredIntervals = [15.0] + rcc.skipBackwardCommand.preferredIntervals = [15.0] + rcc.skipForwardCommand.addTarget { event in + guard let event = event as? MPSkipIntervalCommandEvent else { return .commandFailed } + self.seek(offset: CMTime(seconds: event.interval, preferredTimescale: 1_000)) + return .success + } + rcc.skipBackwardCommand.addTarget { event in + guard let event = event as? MPSkipIntervalCommandEvent else { return .commandFailed } + self.seek(offset: CMTime(seconds: -event.interval, preferredTimescale: 1_000)) + return .success + } + rcc.playCommand.addTarget { _ in + self.play() + return .success + } + rcc.pauseCommand.addTarget { _ in + self.pause() + return .success + } + } + private func doFallbackOnFailed() { if self.avPlayer.currentItem?.status == .failed { guard let url = (avPlayer.currentItem?.asset as? AVURLAsset)?.url else { @@ -534,7 +568,7 @@ public class ApiVideoPlayerController: NSObject { case let .failure(error): print("analytics error on pause event: \(error)") } } - self.infoNowPlaying?.pause(currentTime: self.currentTime, currentRate: self.avPlayer.rate) + self.infoNowPlaying.pause(currentTime: self.currentTime, currentRate: self.avPlayer.rate) self.multicastDelegate.didPause() } @@ -546,7 +580,7 @@ public class ApiVideoPlayerController: NSObject { if self.isFirstPlay { self.isFirstPlay = false #if !os(macOS) - self.infoNowPlaying?.nowPlayingData = NowPlayingData( + self.infoNowPlaying.nowPlayingData = NowPlayingData( duration: self.duration, currentTime: self.currentTime, isLive: self.isLive, @@ -567,7 +601,7 @@ public class ApiVideoPlayerController: NSObject { case let .failure(error): print("analytics error on resume event: \(error)") } } - self.infoNowPlaying?.play(currentTime: self.currentTime, currentRate: self.avPlayer.rate) + self.infoNowPlaying.play(currentTime: self.currentTime, currentRate: self.avPlayer.rate) } #if !os(macOS) try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback) @@ -576,41 +610,6 @@ public class ApiVideoPlayerController: NSObject { self.multicastDelegate.didPlay() } - #if !os(macOS) - func remoteControlEventReceived(with event: UIEvent?) { - if let event = event { - switch event.subtype { - case .remoteControlPlay: - // Handle play remote control event - self.play() - - case .remoteControlPause: - self.pause() - - // Handle pause remote control event - case .remoteControlNextTrack: - self.seek(to: self.duration) - - // Handle next track remote control event - case .remoteControlPreviousTrack: - self.replay() - - // Handle previous track remote control event - case .remoteControlStop: - // Handle stop remote control event - break - - case .remoteControlTogglePlayPause: - // Handle toggle play/pause remote control event - break - - default: - break - } - } - } - #endif - private func doTimeControlStatus() { let status = self.avPlayer.timeControlStatus switch status { diff --git a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift index 46f29f82..19af430d 100644 --- a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift +++ b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift @@ -48,14 +48,6 @@ public class SwiftUIPlayerViewController: UIViewController { super.viewDidDisappear(animated) } - override public func remoteControlReceived(with event: UIEvent?) { - if let event = event { - if event.type == .remoteControl { - self.playerView.remoteControlEventReceived(with: event) - } - } - } - public func play() { self.playerView.play() } diff --git a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift index efcd7634..83d48332 100644 --- a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift +++ b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift @@ -239,10 +239,13 @@ public class ApiVideoPlayerView: UIView { } } - /// Pass events received from remote control, to handle actions. - /// - Parameter event: event of type remoteControl - public func remoteControlEventReceived(with event: UIEvent?) { - self.playerController.remoteControlEventReceived(with: event) + /// Toggle the visibility of the remote control. + /// Setting it to true will display it, while setting it to False will hide it. + /// By default the remote is hidden. + public var enableRemotteControl: Bool = false { + didSet { + self.playerController.enableRemoteControl = enableRemotteControl + } } /// Remove remote control from lockscreen and notification center. From 15fd372fdfcec4ada5ea14502bf3bd3884e80f2a Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Wed, 26 Apr 2023 15:55:10 +0200 Subject: [PATCH 27/55] fix() remove appdelegate events listener --- Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift b/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift index e2a66c75..ebb15c13 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift +++ b/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift @@ -9,7 +9,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. // Allow application to receive events from remote control -// UIApplication.shared.beginReceivingRemoteControlEvents() return true } @@ -32,6 +31,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Use this method to release any resources that were specific to the discarded scenes, as they will not return. // Stop receiving remote control events - UIApplication.shared.endReceivingRemoteControlEvents() } } From 74f734ed6e1eaea0faf456e7d36d5d0a1a030533 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Wed, 26 Apr 2023 15:56:09 +0200 Subject: [PATCH 28/55] fix(sample) remove event listener, add api to display remote control --- .../PlayerUIKit/PlayerUIkit/PlayerViewController.swift | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift b/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift index c59b90eb..8cd1a700 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift +++ b/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift @@ -127,7 +127,7 @@ class PlayerViewController: UIViewController { view.addSubview(self.scrollView) self.scrollView.addSubview(self.playerView) self.playerView.addDelegate(self) - self.playerView.allowRemoteControl() + self.playerView.enableRemotteControl = true let doubleTap = UITapGestureRecognizer(target: self, action: #selector(self.handleDoubleTap(_:))) doubleTap.numberOfTapsRequired = 2 self.playerView.addGestureRecognizer(doubleTap) @@ -184,14 +184,6 @@ class PlayerViewController: UIViewController { self.playerView.viewController = self } - override func remoteControlReceived(with event: UIEvent?) { - if let event = event { - if event.type == .remoteControl { - self.playerView.remoteControlEventReceived(with: event) - } - } - } - private func constraints() { // ScrollView self.scrollView.translatesAutoresizingMaskIntoConstraints = false From 7846fd5574cf8b425ba278b4d385d79460a278bc Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Wed, 26 Apr 2023 15:58:32 +0200 Subject: [PATCH 29/55] fix() add condition if macos --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index b9486d08..030641b3 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -445,7 +445,9 @@ public class ApiVideoPlayerController: NSObject { if enableRemoteControl { self.setupRemoteControls() } else { + #if !os(macOS) UIApplication.shared.endReceivingRemoteControlEvents() + #endif } } } From 7124e67dfdbf6e9ffde46f63616bc76b6bdcde81 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Wed, 26 Apr 2023 16:06:48 +0200 Subject: [PATCH 30/55] chore(swiftui) add getter setter to handle remote control, remove unused method to hide remote control --- .../ApiVideoPlayer/ApiVideoPlayerController.swift | 13 ------------- Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift | 3 +++ .../SwiftUI/SwiftUIPlayerViewController.swift | 6 +----- .../ApiVideoPlayer/Views/ApiVideoPlayerView.swift | 7 ------- 4 files changed, 4 insertions(+), 25 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 030641b3..5a3cacab 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -110,19 +110,6 @@ public class ApiVideoPlayerController: NSObject { multicastDelegate.removeDelegates(delegates) } - /// remove remote control from lockscreen and notification center - func removeRemoteControl() { - self.pause() - #if !os(macOS) - do { - try AVAudioSession.sharedInstance().setActive(false) - } catch { - print("Error deactivating audio session: \(error.localizedDescription)") - } - #endif - self.infoNowPlaying.clearMPNowPlayingInfoCenter() - } - private func resetPlayer(with playerItem: AVPlayerItem? = nil) { if let currentItem = avPlayer.currentItem { currentItem.removeObserver(self, forKeyPath: "status", context: nil) diff --git a/Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift b/Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift index af087473..f7d4b089 100644 --- a/Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift +++ b/Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift @@ -113,6 +113,9 @@ public struct ApiVideoPlayer: UIViewControllerRepresentable { } } + /// Toggle the visibility of the remote control. + /// Setting it to true will display it, while setting it to False will hide it. + /// By default the remote is hidden. public var enableRemoteControl: Bool = false { didSet { self.playerViewController.enableRemoteControl = enableRemoteControl diff --git a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift index 19af430d..5df9a3af 100644 --- a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift +++ b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift @@ -121,11 +121,7 @@ public class SwiftUIPlayerViewController: UIViewController { public var enableRemoteControl: Bool = false { didSet { - if enableRemoteControl { - UIApplication.shared.beginReceivingRemoteControlEvents() - } else { - UIApplication.shared.endReceivingRemoteControlEvents() - } + self.playerView.enableRemotteControl = enableRemoteControl } } diff --git a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift index 83d48332..e85350b1 100644 --- a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift +++ b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift @@ -247,13 +247,6 @@ public class ApiVideoPlayerView: UIView { self.playerController.enableRemoteControl = enableRemotteControl } } - - /// Remove remote control from lockscreen and notification center. - /// This is not definitive, when play again a video the remote control will be displayed. - /// It's recommended to call this function when you're done playing the video. - public func removeRemoteControl() { - self.playerController.removeRemoteControl() - } } #else From 0fca502727d7ec2bfd806372f300da48a6c02f94 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Wed, 26 Apr 2023 16:25:18 +0200 Subject: [PATCH 31/55] fix() swiftlint redoundant annotation --- .../ApiVideoPlayerController.swift | 26 ++++++++++++------- .../SwiftUI/ApiVideoPlayer.swift | 9 ++++--- .../SwiftUI/SwiftUIPlayerViewController.swift | 9 ++++--- .../Views/ApiVideoPlayerView.swift | 9 ++++--- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 5a3cacab..b956bf38 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -19,6 +19,17 @@ public class ApiVideoPlayerController: NSObject { private var storedSpeedRate: Float = 1.0 private var infoNowPlaying: ApiVideoPlayerInformationNowPlaying private let rcc = MPRemoteCommandCenter.shared() + private var enableRC = false { + didSet { + if enableRC { + self.setupRemoteControls() + } else { + #if !os(macOS) + UIApplication.shared.endReceivingRemoteControlEvents() + #endif + } + } + } #if !os(macOS) /// Initializes a player controller. @@ -427,15 +438,12 @@ public class ApiVideoPlayerController: NSObject { } } - public var enableRemoteControl: Bool = false { - didSet { - if enableRemoteControl { - self.setupRemoteControls() - } else { - #if !os(macOS) - UIApplication.shared.endReceivingRemoteControlEvents() - #endif - } + public var enableRemoteControl: Bool { + get { + self.enableRC + } + set(newValue) { + self.enableRC = newValue } } diff --git a/Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift b/Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift index f7d4b089..12ea28c9 100644 --- a/Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift +++ b/Sources/ApiVideoPlayer/SwiftUI/ApiVideoPlayer.swift @@ -116,9 +116,12 @@ public struct ApiVideoPlayer: UIViewControllerRepresentable { /// Toggle the visibility of the remote control. /// Setting it to true will display it, while setting it to False will hide it. /// By default the remote is hidden. - public var enableRemoteControl: Bool = false { - didSet { - self.playerViewController.enableRemoteControl = enableRemoteControl + public var enableRemoteControl: Bool { + get { + self.playerViewController.enableRemoteControl + } + set(newValue) { + self.playerViewController.enableRemoteControl = newValue } } diff --git a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift index 5df9a3af..42c011e2 100644 --- a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift +++ b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift @@ -119,9 +119,12 @@ public class SwiftUIPlayerViewController: UIViewController { } } - public var enableRemoteControl: Bool = false { - didSet { - self.playerView.enableRemotteControl = enableRemoteControl + public var enableRemoteControl: Bool { + get { + self.playerView.enableRemotteControl + } + set(newValue) { + self.playerView.enableRemotteControl = newValue } } diff --git a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift index e85350b1..f4a4e6e7 100644 --- a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift +++ b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift @@ -242,9 +242,12 @@ public class ApiVideoPlayerView: UIView { /// Toggle the visibility of the remote control. /// Setting it to true will display it, while setting it to False will hide it. /// By default the remote is hidden. - public var enableRemotteControl: Bool = false { - didSet { - self.playerController.enableRemoteControl = enableRemotteControl + public var enableRemotteControl: Bool { + get { + self.playerController.enableRemoteControl + } + set(newValue) { + self.playerController.enableRemoteControl = newValue } } } From 20bfb792c823aa4afc78a7f9ca6ef208433700ae Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Wed, 26 Apr 2023 16:25:50 +0200 Subject: [PATCH 32/55] chore() remove 'clearMPNowPlayingInfoCenter' func by setting 'nowPlayingData' to nil --- .../ApiVideoPlayerInformationNowPlaying.swift | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift index 27411c39..496b07bd 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -6,7 +6,6 @@ protocol InformationNowPlaying { func pause(currentTime: CMTime, currentRate: Float) func play(currentTime: CMTime, currentRate: Float) func overrideInformations(for key: String, value: Any) - func clearMPNowPlayingInfoCenter() } class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { @@ -19,21 +18,20 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { var nowPlayingData: NowPlayingData? { didSet { - if let title = nowPlayingData?.title { - infos[MPMediaItemPropertyTitle] = title - } - if let duration = nowPlayingData?.duration { - infos[MPMediaItemPropertyPlaybackDuration] = duration.seconds + guard let nowPlayingData = nowPlayingData else { + MPNowPlayingInfoCenter.default().nowPlayingInfo = [:] + return } - if let currentTime = nowPlayingData?.currentTime { - infos[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime.seconds - } - if let live = nowPlayingData?.isLive { - infos[MPNowPlayingInfoPropertyIsLiveStream] = live + if let title = nowPlayingData.title { + infos[MPMediaItemPropertyTitle] = title } + infos[MPMediaItemPropertyPlaybackDuration] = nowPlayingData.duration.seconds + infos[MPNowPlayingInfoPropertyElapsedPlaybackTime] = nowPlayingData.currentTime.seconds + infos[MPNowPlayingInfoPropertyIsLiveStream] = nowPlayingData.isLive + infos[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 #if !os(macOS) - if let thumb = nowPlayingData?.thumbnailUrl { + if let thumb = nowPlayingData.thumbnailUrl { if let url = URL(string: thumb) { updateRemoteArtwork(url: url) } @@ -60,10 +58,6 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { MPNowPlayingInfoCenter.default().nowPlayingInfo = infos } - func clearMPNowPlayingInfoCenter() { - MPNowPlayingInfoCenter.default().nowPlayingInfo = [:] - } - #if !os(macOS) private func getArtwork(image: UIImage) -> MPMediaItemArtwork { return MPMediaItemArtwork(boundsSize: image.size) { _ in image } From 96f233d1f8459256277175d9bf43bf9524c36e47 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Wed, 26 Apr 2023 16:32:17 +0200 Subject: [PATCH 33/55] chore() remove old doc, update readme --- README.md | 11 ++- .../Adding video player remote control.md | 85 ------------------- .../Documentation.docc/Documentation.md | 1 - 3 files changed, 10 insertions(+), 87 deletions(-) delete mode 100644 Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md diff --git a/README.md b/README.md index 812337b7..b61e04fb 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,16 @@ override func viewDidAppear(_ animated: Bool) { ``` ### Remote control -If you want to enable the remote control please check this [documentation](https://apivideo.github.io/api.video-swift-player/documentation/apivideoplayer/adding-video-player-remote-control) +If you want to enable the remote control do the following : +```swift +override func viewDidLoad() { + ... + self.playerView.enableRemoteControl = true +} +``` +When you have to remove it set `enableRemoteControl` to false + +By default the remote control is hidden. # Sample application diff --git a/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md b/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md deleted file mode 100644 index 733389ba..00000000 --- a/Sources/ApiVideoPlayer/Documentation.docc/Adding video player remote control.md +++ /dev/null @@ -1,85 +0,0 @@ -# Adding video player remote control - -On this article we will dive on how to display a remote controller on your lockscreen and notification center - -## Table of contents - -- [Receive events from remote-control](#receive-events-from-remote-control) - - [UIKit](#uikit) - - [SwiftUI](#swiftui) -- [Documentation](#documentation) -- [Dependencies](#dependencies) - -## Receive events from remote-control -### UIKit -On your AppDelegate.swift, - - add in 'didFinishLaunchingWithOptions' function: -``` -// Start receiving events -UIApplication.shared.beginReceivingRemoteControlEvents() -``` - - add in 'didDiscardSceneSessions' function: -``` -// Stop receiving events -UIApplication.shared.endReceivingRemoteControlEvents() -``` -Then in your ViewController.swift - -After you have instanciated the player, call the method 'allowRemoteControl' to send data to the remote control - -``` -override func viewDidLoad() { -... -self.playerView.allowRemoteControl() -... -``` - -Now you need to override the 'remoteControlReceived' function and pass the events to 'remoteControlEventReceived' of ApiVideoSwiftPlayer - -``` -override func remoteControlReceived(with event: UIEvent?) { - if let event = event { - if event.type == .remoteControl { - self.playerView.remoteControlEventReceived(with: event) - } - } -} -``` - -### SwiftUI -For SwiftUI you don't have any to do, it's already handle by the library. - -## Remove remote control -You can use the 'removeRemoteControl' function to take out the remote control. - -For instance, when a video reaches its end, you have the option to remove it. -It's important to note that using this method will pause the video, and if you play it again, the remote control will be displayed one more. -``` -public func didEnd() { -... - self.playerView.removeRemoteControl() -... - -} -``` - -That's it, now you can test it and play with the remote control. - - -## Documentation - -* [API documentation](https://apivideo.github.io/api.video-swift-player/documentation/apivideoplayer/) -* [api.video documentation](https://docs.api.video) - -## Dependencies - -We are using external library - -| Plugin | README | -|---------------------------------------------------------------------------------------|--------------------------------------------------------------------------------| -| [ApiVideoPlayerAnalytics](https://github.com/apivideo/api.video-ios-player-analytics) | [README.md](https://github.com/apivideo/api.video-ios-player-analytics#readme) | - -## FAQ - -If you have any questions, ask us here: [https://community.api.video](https://community.api.video) or -use [Issues](https://github.com/apivideo/api.video-ios-player-analytics/issues). diff --git a/Sources/ApiVideoPlayer/Documentation.docc/Documentation.md b/Sources/ApiVideoPlayer/Documentation.docc/Documentation.md index 7e8a01f0..c9b48ea3 100644 --- a/Sources/ApiVideoPlayer/Documentation.docc/Documentation.md +++ b/Sources/ApiVideoPlayer/Documentation.docc/Documentation.md @@ -10,7 +10,6 @@ Swift library to add player for your videos stored at api.video. ### Essentials - -- ### UIKit - ``ApiVideoPlayerView`` From b9c8a5ba03c78343ec2d383564875cc84e889ef0 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Wed, 26 Apr 2023 16:43:25 +0200 Subject: [PATCH 34/55] fix() make overrideInformations private, add getter setter currentTime and playbackRate --- .../ApiVideoPlayerController.swift | 5 +--- .../ApiVideoPlayerInformationNowPlaying.swift | 29 +++++++++++++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index b956bf38..bf08ec32 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -240,10 +240,7 @@ public class ApiVideoPlayerController: NSObject { case let .failure(error): print("analytics error on seek event: \(error)") } } - self.infoNowPlaying.overrideInformations( - for: MPNowPlayingInfoPropertyElapsedPlaybackTime, - value: self.currentTime.seconds - ) + self.infoNowPlaying.currentTime = self.currentTime completion(completed) } } diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift index 496b07bd..739354a5 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -3,9 +3,10 @@ import MediaPlayer protocol InformationNowPlaying { var nowPlayingData: NowPlayingData? { get set } + var currentTime: CMTime? { get set } + var playbackRate: Float? { get set } func pause(currentTime: CMTime, currentRate: Float) func play(currentTime: CMTime, currentRate: Float) - func overrideInformations(for key: String, value: Any) } class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { @@ -16,6 +17,30 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { self.taskExecutor = taskExecutor } + var currentTime: CMTime? { + didSet { + guard let currentTime = currentTime else { + return + } + self.overrideInformations( + for: MPNowPlayingInfoPropertyElapsedPlaybackTime, + value: currentTime.seconds + ) + } + } + + var playbackRate: Float? { + didSet { + guard let playbackRate = playbackRate else { + return + } + self.overrideInformations( + for: MPNowPlayingInfoPropertyPlaybackRate, + value: playbackRate + ) + } + } + var nowPlayingData: NowPlayingData? { didSet { guard let nowPlayingData = nowPlayingData else { @@ -53,7 +78,7 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: currentTime.seconds) } - func overrideInformations(for key: String, value: Any) { + private func overrideInformations(for key: String, value: Any) { infos[key] = value MPNowPlayingInfoCenter.default().nowPlayingInfo = infos } From 84fd0e86785884e68da509675e82b235b0c1ff2c Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Thu, 27 Apr 2023 14:43:56 +0200 Subject: [PATCH 35/55] chore() rewording --- .../ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift | 4 ++-- Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift index 42c011e2..aad43dbd 100644 --- a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift +++ b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift @@ -121,10 +121,10 @@ public class SwiftUIPlayerViewController: UIViewController { public var enableRemoteControl: Bool { get { - self.playerView.enableRemotteControl + self.playerView.enableRemoteControl } set(newValue) { - self.playerView.enableRemotteControl = newValue + self.playerView.enableRemoteControl = newValue } } diff --git a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift index f4a4e6e7..24118d27 100644 --- a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift +++ b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift @@ -242,7 +242,7 @@ public class ApiVideoPlayerView: UIView { /// Toggle the visibility of the remote control. /// Setting it to true will display it, while setting it to False will hide it. /// By default the remote is hidden. - public var enableRemotteControl: Bool { + public var enableRemoteControl: Bool { get { self.playerController.enableRemoteControl } From 835dfc8cc0fa77865796dae87dc821488a08c371 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Thu, 27 Apr 2023 15:55:42 +0200 Subject: [PATCH 36/55] fix() rename func, delete 'removeRemoteControl' func --- Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift b/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift index 8cd1a700..369e8c55 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift +++ b/Examples/PlayerUIKit/PlayerUIkit/PlayerViewController.swift @@ -127,7 +127,7 @@ class PlayerViewController: UIViewController { view.addSubview(self.scrollView) self.scrollView.addSubview(self.playerView) self.playerView.addDelegate(self) - self.playerView.enableRemotteControl = true + self.playerView.enableRemoteControl = true let doubleTap = UITapGestureRecognizer(target: self, action: #selector(self.handleDoubleTap(_:))) doubleTap.numberOfTapsRequired = 2 self.playerView.addGestureRecognizer(doubleTap) @@ -331,7 +331,6 @@ extension PlayerViewController: ApiVideoPlayerControllerPlayerDelegate { } public func didEnd() { - self.playerView.removeRemoteControl() print("app didEnd") } From 1b0ff0003082c5fdd79414ae8f35426dbbd679da Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Thu, 27 Apr 2023 15:56:13 +0200 Subject: [PATCH 37/55] chore() use 'MPRemoteCommandCenter' only in 'setupRemoteControls' --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index bf08ec32..102c8d82 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -18,7 +18,6 @@ public class ApiVideoPlayerController: NSObject { private var playerItemFactory: ApiVideoPlayerItemFactory? private var storedSpeedRate: Float = 1.0 private var infoNowPlaying: ApiVideoPlayerInformationNowPlaying - private let rcc = MPRemoteCommandCenter.shared() private var enableRC = false { didSet { if enableRC { @@ -494,6 +493,7 @@ public class ApiVideoPlayerController: NSObject { } private func setupRemoteControls() { + let rcc = MPRemoteCommandCenter.shared() rcc.skipForwardCommand.preferredIntervals = [15.0] rcc.skipBackwardCommand.preferredIntervals = [15.0] rcc.skipForwardCommand.addTarget { event in From 3e79588bb2f48d26ede6acb81ca54800ee6778e7 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Thu, 27 Apr 2023 15:57:02 +0200 Subject: [PATCH 38/55] chore(readme) rewording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b61e04fb..a31acc6f 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ override func viewDidAppear(_ animated: Bool) { ``` ### Remote control -If you want to enable the remote control do the following : +If you want to enable the remote control do the following: ```swift override func viewDidLoad() { ... From 79e16bfe4d3eb7c82d5575df3506ad3ed3b23b5d Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Fri, 28 Apr 2023 11:01:32 +0200 Subject: [PATCH 39/55] fix() replace getter/setter by func for 'currentTime' and 'playbackRate', remove redundant variable to enable remote control --- .../ApiVideoPlayerController.swift | 26 ++++-------- .../ApiVideoPlayerInformationNowPlaying.swift | 42 +++++++------------ 2 files changed, 25 insertions(+), 43 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 102c8d82..fbe82d2b 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -18,17 +18,6 @@ public class ApiVideoPlayerController: NSObject { private var playerItemFactory: ApiVideoPlayerItemFactory? private var storedSpeedRate: Float = 1.0 private var infoNowPlaying: ApiVideoPlayerInformationNowPlaying - private var enableRC = false { - didSet { - if enableRC { - self.setupRemoteControls() - } else { - #if !os(macOS) - UIApplication.shared.endReceivingRemoteControlEvents() - #endif - } - } - } #if !os(macOS) /// Initializes a player controller. @@ -434,12 +423,15 @@ public class ApiVideoPlayerController: NSObject { } } - public var enableRemoteControl: Bool { - get { - self.enableRC - } - set(newValue) { - self.enableRC = newValue + public var enableRemoteControl = false { + didSet { + if enableRemoteControl { + self.setupRemoteControls() + } else { + #if !os(macOS) + UIApplication.shared.endReceivingRemoteControlEvents() + #endif + } } } diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift index 739354a5..688f49ea 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -3,10 +3,10 @@ import MediaPlayer protocol InformationNowPlaying { var nowPlayingData: NowPlayingData? { get set } - var currentTime: CMTime? { get set } - var playbackRate: Float? { get set } func pause(currentTime: CMTime, currentRate: Float) func play(currentTime: CMTime, currentRate: Float) + func updateCurrentTime(currentTime: CMTime) + func updatePlabackRate(rate: Float) } class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { @@ -17,30 +17,6 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { self.taskExecutor = taskExecutor } - var currentTime: CMTime? { - didSet { - guard let currentTime = currentTime else { - return - } - self.overrideInformations( - for: MPNowPlayingInfoPropertyElapsedPlaybackTime, - value: currentTime.seconds - ) - } - } - - var playbackRate: Float? { - didSet { - guard let playbackRate = playbackRate else { - return - } - self.overrideInformations( - for: MPNowPlayingInfoPropertyPlaybackRate, - value: playbackRate - ) - } - } - var nowPlayingData: NowPlayingData? { didSet { guard let nowPlayingData = nowPlayingData else { @@ -66,6 +42,20 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { } } + func updateCurrentTime(currentTime: CMTime) { + self.overrideInformations( + for: MPNowPlayingInfoPropertyElapsedPlaybackTime, + value: currentTime.seconds + ) + } + + func updatePlabackRate(rate: Float) { + self.overrideInformations( + for: MPNowPlayingInfoPropertyPlaybackRate, + value: rate + ) + } + func pause(currentTime: CMTime, currentRate: Float) { infos[MPNowPlayingInfoPropertyPlaybackRate] = currentRate MPNowPlayingInfoCenter.default().playbackState = .paused From dbe38634599597173621ad9353d04e36e78f821a Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Fri, 28 Apr 2023 11:05:19 +0200 Subject: [PATCH 40/55] fix() use new 'updateCurrentTime' func, remove setter usage --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index fbe82d2b..4f0f9104 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -228,7 +228,7 @@ public class ApiVideoPlayerController: NSObject { case let .failure(error): print("analytics error on seek event: \(error)") } } - self.infoNowPlaying.currentTime = self.currentTime + self.infoNowPlaying.updateCurrentTime(currentTime: from) completion(completed) } } @@ -419,7 +419,7 @@ public class ApiVideoPlayerController: NSObject { if isPlaying { avPlayer.rate = newRate } - + infoNowPlaying.updatePlabackRate(rate: newRate) } } From 16f8ccd3fb807d49aa0ee6cba556b92529f300de Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Fri, 28 Apr 2023 14:41:26 +0200 Subject: [PATCH 41/55] feat() update infoNowPlaying rate with notification on ios 15+ else on user interaction --- .../ApiVideoPlayerController.swift | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 4f0f9104..5bceb555 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -75,6 +75,11 @@ public class ApiVideoPlayerController: NSObject { options: NSKeyValueObservingOptions.new, context: nil ) + if #available(iOS 15.0, *) { + NotificationCenter.default.addObserver(self, selector: #selector(handlePlaybackRateChange(_:)), name: AVPlayer.rateDidChangeNotification, object: self.avPlayer) + } else { + // Fallback on earlier versions + } } private func retrySetUpPlayerUrlWithMp4() { @@ -419,7 +424,13 @@ public class ApiVideoPlayerController: NSObject { if isPlaying { avPlayer.rate = newRate } - infoNowPlaying.updatePlabackRate(rate: newRate) + if #available(iOS 15, *) { + // do nothing Notification will handle updatePlaybackRate + } else { + // iOS version is less than iOS 15 + infoNowPlaying.updatePlabackRate(rate: newRate) + infoNowPlaying.updateCurrentTime(currentTime: self.currentTime) + } } } @@ -595,6 +606,15 @@ public class ApiVideoPlayerController: NSObject { #endif self.multicastDelegate.didPlay() } + + @objc + func handlePlaybackRateChange(_ notification: Notification) { + guard let player = notification.object as? AVPlayer else { + return + } + infoNowPlaying.updatePlabackRate(rate: player.rate) + infoNowPlaying.updateCurrentTime(currentTime: self.currentTime) + } private func doTimeControlStatus() { let status = self.avPlayer.timeControlStatus From 98d6a97d2f9f34eabc64f09ef3cd512b02873f89 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Fri, 28 Apr 2023 14:41:47 +0200 Subject: [PATCH 42/55] chore() format --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 5bceb555..caf02aeb 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -76,7 +76,12 @@ public class ApiVideoPlayerController: NSObject { context: nil ) if #available(iOS 15.0, *) { - NotificationCenter.default.addObserver(self, selector: #selector(handlePlaybackRateChange(_:)), name: AVPlayer.rateDidChangeNotification, object: self.avPlayer) + NotificationCenter.default.addObserver( + self, + selector: #selector(handlePlaybackRateChange(_:)), + name: AVPlayer.rateDidChangeNotification, + object: self.avPlayer + ) } else { // Fallback on earlier versions } @@ -606,7 +611,7 @@ public class ApiVideoPlayerController: NSObject { #endif self.multicastDelegate.didPlay() } - + @objc func handlePlaybackRateChange(_ notification: Notification) { guard let player = notification.object as? AVPlayer else { From de27b0a93da7ee155e37bd78618ac4178b3ad9ed Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Fri, 28 Apr 2023 14:58:12 +0200 Subject: [PATCH 43/55] fix() add condition for macos --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index caf02aeb..79d6c421 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -75,7 +75,7 @@ public class ApiVideoPlayerController: NSObject { options: NSKeyValueObservingOptions.new, context: nil ) - if #available(iOS 15.0, *) { + if #available(iOS 15.0, macOS 12.0, *) { NotificationCenter.default.addObserver( self, selector: #selector(handlePlaybackRateChange(_:)), From 849b7024d0345b53e7adca989f98d9a6d865979f Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Fri, 28 Apr 2023 15:20:14 +0200 Subject: [PATCH 44/55] chore() remove comments --- Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift b/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift index ebb15c13..e585210d 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift +++ b/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift @@ -7,8 +7,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Override point for customization after application launch. - - // Allow application to receive events from remote control return true } @@ -29,7 +27,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // If any sessions were discarded while the application was not running, this will be called shortly after // application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - - // Stop receiving remote control events } } From d3fdbade4afe0208083941b935287ac108e6471e Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Fri, 28 Apr 2023 15:20:14 +0200 Subject: [PATCH 45/55] chore() rm swiftui info.plist --- Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist | 5 ----- Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift | 4 ---- 2 files changed, 9 deletions(-) delete mode 100644 Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist diff --git a/Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist b/Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist deleted file mode 100644 index 0c67376e..00000000 --- a/Examples/PlayerSwiftUI/PlayerSwiftUI/Info.plist +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift b/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift index ebb15c13..e585210d 100644 --- a/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift +++ b/Examples/PlayerUIKit/PlayerUIkit/AppDelegate.swift @@ -7,8 +7,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Override point for customization after application launch. - - // Allow application to receive events from remote control return true } @@ -29,7 +27,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // If any sessions were discarded while the application was not running, this will be called shortly after // application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - - // Stop receiving remote control events } } From a823a7db28804071016172150f00feb0f6d5bd63 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Fri, 28 Apr 2023 15:44:31 +0200 Subject: [PATCH 46/55] fix() don't update 'NowPlayingInfoCenter' from AVPlayerViewController --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 79d6c421..416b7cfe 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -479,6 +479,7 @@ public class ApiVideoPlayerController: NSObject { public func goToFullScreen(viewController: UIViewController) { let playerViewController = AVPlayerViewController() playerViewController.player = self.avPlayer + playerViewController.updatesNowPlayingInfoCenter = false viewController.present(playerViewController, animated: true) { self.play() } From 50808d22436332cb2de43dae32c54428beb87232 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 9 May 2023 16:18:57 +0200 Subject: [PATCH 47/55] chore(readme) add EOL --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a31acc6f..67e53514 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,7 @@ override func viewDidAppear(_ animated: Bool) { ``` ### Remote control + If you want to enable the remote control do the following: ```swift override func viewDidLoad() { From 371911f9ab81b9e52567acbb8f3afec0ed6699b2 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 9 May 2023 16:23:43 +0200 Subject: [PATCH 48/55] chore() use 'to' instead of 'from' in 'updateCurrentTime' func --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 416b7cfe..c2fef0a6 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -231,14 +231,14 @@ public class ApiVideoPlayerController: NSObject { self.analytics? .seek( from: Float(CMTimeGetSeconds(from)), - to: Float(CMTimeGetSeconds(self.currentTime)) + to: Float(time.seconds) ) { result in switch result { case .success: break case let .failure(error): print("analytics error on seek event: \(error)") } } - self.infoNowPlaying.updateCurrentTime(currentTime: from) + self.infoNowPlaying.updateCurrentTime(currentTime: time) completion(completed) } } From 8cea54d1f2e1a57f00ffd06660d5418c9594c8f8 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 9 May 2023 16:24:18 +0200 Subject: [PATCH 49/55] chore() remove call of 'updateCurrentTime' func after 'updatePlaybackRate' --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index c2fef0a6..4f087342 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -433,8 +433,7 @@ public class ApiVideoPlayerController: NSObject { // do nothing Notification will handle updatePlaybackRate } else { // iOS version is less than iOS 15 - infoNowPlaying.updatePlabackRate(rate: newRate) - infoNowPlaying.updateCurrentTime(currentTime: self.currentTime) + infoNowPlaying.updatePlaybackRate(rate: newRate) } } } @@ -618,8 +617,7 @@ public class ApiVideoPlayerController: NSObject { guard let player = notification.object as? AVPlayer else { return } - infoNowPlaying.updatePlabackRate(rate: player.rate) - infoNowPlaying.updateCurrentTime(currentTime: self.currentTime) + infoNowPlaying.updatePlaybackRate(rate: player.rate) } private func doTimeControlStatus() { From afd4fffebf92f86879fc68bbe19406b41afd1b8a Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 9 May 2023 16:26:23 +0200 Subject: [PATCH 50/55] chore() remove 'currentRate' of play and pause func, reword 'updatePlaybackRate' --- .../ApiVideoPlayer/ApiVideoPlayerController.swift | 4 ++-- .../ApiVideoPlayerInformationNowPlaying.swift | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 4f087342..6a7e318f 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -570,7 +570,7 @@ public class ApiVideoPlayerController: NSObject { case let .failure(error): print("analytics error on pause event: \(error)") } } - self.infoNowPlaying.pause(currentTime: self.currentTime, currentRate: self.avPlayer.rate) + self.infoNowPlaying.pause(currentTime: self.currentTime) self.multicastDelegate.didPause() } @@ -603,7 +603,7 @@ public class ApiVideoPlayerController: NSObject { case let .failure(error): print("analytics error on resume event: \(error)") } } - self.infoNowPlaying.play(currentTime: self.currentTime, currentRate: self.avPlayer.rate) + self.infoNowPlaying.play(currentTime: self.currentTime) } #if !os(macOS) try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift index 688f49ea..718ea163 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -3,10 +3,10 @@ import MediaPlayer protocol InformationNowPlaying { var nowPlayingData: NowPlayingData? { get set } - func pause(currentTime: CMTime, currentRate: Float) - func play(currentTime: CMTime, currentRate: Float) + func pause(currentTime: CMTime) + func play(currentTime: CMTime) func updateCurrentTime(currentTime: CMTime) - func updatePlabackRate(rate: Float) + func updatePlaybackRate(rate: Float) } class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { @@ -49,21 +49,19 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { ) } - func updatePlabackRate(rate: Float) { + func updatePlaybackRate(rate: Float) { self.overrideInformations( for: MPNowPlayingInfoPropertyPlaybackRate, value: rate ) } - func pause(currentTime: CMTime, currentRate: Float) { - infos[MPNowPlayingInfoPropertyPlaybackRate] = currentRate + func pause(currentTime: CMTime) { MPNowPlayingInfoCenter.default().playbackState = .paused self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: currentTime.seconds) } - func play(currentTime: CMTime, currentRate: Float) { - infos[MPNowPlayingInfoPropertyPlaybackRate] = currentRate + func play(currentTime: CMTime) { MPNowPlayingInfoCenter.default().playbackState = .playing self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: currentTime.seconds) } From 2aee004a384ba69d87a28259bd4832c119e6f615 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 9 May 2023 16:26:55 +0200 Subject: [PATCH 51/55] chore() add comment for fullscreen infocenter --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 6a7e318f..5472e5b8 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -478,6 +478,7 @@ public class ApiVideoPlayerController: NSObject { public func goToFullScreen(viewController: UIViewController) { let playerViewController = AVPlayerViewController() playerViewController.player = self.avPlayer + // set updatesNowPlayingInfoCenter to false to avoid issue with artwork (blink when play/pause video) playerViewController.updatesNowPlayingInfoCenter = false viewController.present(playerViewController, animated: true) { self.play() From 0b2d6bc005c51e347a85caf3c06da215d294c931 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 9 May 2023 16:27:23 +0200 Subject: [PATCH 52/55] chore() use 'enableRemoteControl' --- .../ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift index aad43dbd..346d822e 100644 --- a/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift +++ b/Sources/ApiVideoPlayer/SwiftUI/SwiftUIPlayerViewController.swift @@ -44,7 +44,7 @@ public class SwiftUIPlayerViewController: UIViewController { override public func viewDidDisappear(_ animated: Bool) { self.playerView.viewController = nil - UIApplication.shared.endReceivingRemoteControlEvents() + enableRemoteControl = false super.viewDidDisappear(animated) } From 1f7ecc8de2fb6712fae88fe9dbb5594a0b93cccd Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 9 May 2023 17:34:54 +0200 Subject: [PATCH 53/55] chore() add 'playbackRate' in 'NowPlayingData' to avoid hard code --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 3 ++- .../ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift | 2 +- Sources/ApiVideoPlayer/Models/NowPlayingData.swift | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index 5472e5b8..ee178b3e 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -587,7 +587,8 @@ public class ApiVideoPlayerController: NSObject { duration: self.duration, currentTime: self.currentTime, isLive: self.isLive, - thumbnailUrl: self.videoOptions?.thumbnailUrl + thumbnailUrl: self.videoOptions?.thumbnailUrl, + playbackRate: self.avPlayer.rate ) #endif diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift index 718ea163..f773e217 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift @@ -30,7 +30,7 @@ class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying { infos[MPNowPlayingInfoPropertyElapsedPlaybackTime] = nowPlayingData.currentTime.seconds infos[MPNowPlayingInfoPropertyIsLiveStream] = nowPlayingData.isLive - infos[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 + infos[MPNowPlayingInfoPropertyPlaybackRate] = nowPlayingData.playbackRate #if !os(macOS) if let thumb = nowPlayingData.thumbnailUrl { if let url = URL(string: thumb) { diff --git a/Sources/ApiVideoPlayer/Models/NowPlayingData.swift b/Sources/ApiVideoPlayer/Models/NowPlayingData.swift index 4c0a15e0..651ea228 100644 --- a/Sources/ApiVideoPlayer/Models/NowPlayingData.swift +++ b/Sources/ApiVideoPlayer/Models/NowPlayingData.swift @@ -7,4 +7,5 @@ struct NowPlayingData { let isLive: Bool let thumbnailUrl: String? let title: String? = nil + let playbackRate: Float } From 7f21b38506241466c2fd46bc1659f5c276bd1b80 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Tue, 9 May 2023 17:36:16 +0200 Subject: [PATCH 54/55] chore() use .seconds instead of convert with 'CMTimeGetSeconds' --- Sources/ApiVideoPlayer/ApiVideoPlayerController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift index ee178b3e..d6cc6aac 100644 --- a/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift +++ b/Sources/ApiVideoPlayer/ApiVideoPlayerController.swift @@ -230,7 +230,7 @@ public class ApiVideoPlayerController: NSObject { self.avPlayer.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero) { completed in self.analytics? .seek( - from: Float(CMTimeGetSeconds(from)), + from: Float(from.seconds), to: Float(time.seconds) ) { result in switch result { From 9c9c98c9c0ad70989b266b3395c82afc7cb85fc6 Mon Sep 17 00:00:00 2001 From: Romain Petit Date: Wed, 10 May 2023 11:20:03 +0200 Subject: [PATCH 55/55] feat() add playbackRate get set to allow user to change it --- Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift index 24118d27..d0ca8246 100644 --- a/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift +++ b/Sources/ApiVideoPlayer/Views/ApiVideoPlayerView.swift @@ -250,6 +250,16 @@ public class ApiVideoPlayerView: UIView { self.playerController.enableRemoteControl = newValue } } + + /// Gets and sets the current playback speed rate. Expected values are between 0.5 and 2.0. + public var playbackRate: Float { + get { + self.playerController.speedRate + } + set(newRate) { + self.playerController.speedRate = newRate + } + } } #else