diff --git a/Client/Frontend/Browser/Playlist/Controller/PlaylistCarplayController.swift b/Client/Frontend/Browser/Playlist/Controller/PlaylistCarplayController.swift index fa9ab23549e..e1e262702c0 100644 --- a/Client/Frontend/Browser/Playlist/Controller/PlaylistCarplayController.swift +++ b/Client/Frontend/Browser/Playlist/Controller/PlaylistCarplayController.swift @@ -21,7 +21,6 @@ class PlaylistCarplayController: NSObject { private var assetStateObservers = Set() private var assetLoadingStateObservers = Set() private var playlistItemIds = [String]() - private var currentlyPlayingItemIndex = -1 init(player: MediaPlayer, contentManager: MPPlayableContentManager) { self.player = player @@ -38,7 +37,12 @@ class PlaylistCarplayController: NSObject { contentManager.dataSource = self contentManager.delegate = self - contentManager.reloadData() + + DispatchQueue.main.async { + contentManager.beginUpdates() + contentManager.endUpdates() + contentManager.reloadData() + } // Workaround to see carplay NowPlaying on the simulator #if targetEnvironment(simulator) @@ -49,6 +53,12 @@ class PlaylistCarplayController: NSObject { #endif } + deinit { +// contentManager.delegate = nil +// contentManager.dataSource = nil +// contentManager.reloadData() + } + func observePlayerStates() { player.publisher(for: .play).sink { _ in MPNowPlayingInfoCenter.default().playbackState = .playing @@ -63,9 +73,7 @@ class PlaylistCarplayController: NSObject { }.store(in: &playerStateObservers) player.publisher(for: .changePlaybackRate).sink { [weak self] _ in - guard let self = self else { return } - - MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self.player.rate + MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self?.player.rate }.store(in: &playerStateObservers) player.publisher(for: .previousTrack).sink { [weak self] _ in @@ -77,11 +85,9 @@ class PlaylistCarplayController: NSObject { }.store(in: &playerStateObservers) player.publisher(for: .finishedPlaying).sink { [weak self] event in - guard let self = self else { return } - event.mediaPlayer.pause() event.mediaPlayer.seek(to: .zero) - self.onNextTrack(isUserInitiated: false) + self?.onNextTrack(isUserInitiated: false) }.store(in: &playerStateObservers) } } @@ -89,7 +95,7 @@ class PlaylistCarplayController: NSObject { extension PlaylistCarplayController: MPPlayableContentDelegate { func playableContentManager(_ contentManager: MPPlayableContentManager, didUpdate context: MPPlayableContentManagerContext) { - PlaylistCarplayManager.shared.attemptInterfaceConnection(isCarPlayAvailable: context.endpointAvailable) + log.debug("CAR PLAY CONNECTED: \(context.endpointAvailable)") } func playableContentManager(_ contentManager: MPPlayableContentManager, initiatePlaybackOfContentItemAt indexPath: IndexPath, completionHandler: @escaping (Error?) -> Void) { @@ -103,7 +109,9 @@ extension PlaylistCarplayController: MPPlayableContentDelegate { } self.contentManager.nowPlayingIdentifiers = [mediaItem.src] - self.playItem(item: mediaItem) { error in + self.playItem(item: mediaItem) { [weak self] error in + PlaylistCarplayManager.shared.currentPlaylistItem = nil + switch error { case .other(let error): log.error(error) @@ -111,7 +119,15 @@ extension PlaylistCarplayController: MPPlayableContentDelegate { case .expired: completionHandler(Strings.PlayList.expiredAlertDescription) case .none: - self.currentlyPlayingItemIndex = indexPath.item + guard let self = self else { + completionHandler("Unknown Error") + return + } + + PlaylistCarplayManager.shared.currentlyPlayingItemIndex = indexPath.item + PlaylistCarplayManager.shared.currentPlaylistItem = mediaItem + PlaylistMediaStreamer.setNowPlayingMediaArtwork(artwork: self.contentItem(at: indexPath)?.artwork) + completionHandler(nil) case .cancelled: log.debug("User Cancelled Playlist playback") @@ -134,8 +150,9 @@ extension PlaylistCarplayController: MPPlayableContentDelegate { } func beginLoadingChildItems(at indexPath: IndexPath, completionHandler: @escaping (Error?) -> Void) { + // For some odd reason, this is never called in the simulator. + // It is only called in the car and that's fine. completionHandler(nil) - contentManager.reloadData() } } @@ -195,15 +212,15 @@ extension PlaylistCarplayController: MPPlayableContentDataSource { extension PlaylistCarplayController { func onPreviousTrack(isUserInitiated: Bool) { - if currentlyPlayingItemIndex <= 0 { + if PlaylistCarplayManager.shared.currentlyPlayingItemIndex <= 0 { return } - let index = currentlyPlayingItemIndex - 1 + let index = PlaylistCarplayManager.shared.currentlyPlayingItemIndex - 1 if index < PlaylistManager.shared.numberOfAssets, let item = PlaylistManager.shared.itemAtIndex(index) { - self.currentlyPlayingItemIndex = index - self.playItem(item: item) { [weak self] error in + PlaylistCarplayManager.shared.currentlyPlayingItemIndex = index + playItem(item: item) { [weak self] error in guard let self = self else { return } switch error { @@ -213,7 +230,7 @@ extension PlaylistCarplayController { case .expired: self.displayExpiredResourceError(item: item) case .none: - self.currentlyPlayingItemIndex = index + PlaylistCarplayManager.shared.currentlyPlayingItemIndex = index self.updateLastPlayedItem(item: item) case .cancelled: log.debug("User Cancelled Playlist Playback") @@ -224,8 +241,8 @@ extension PlaylistCarplayController { func onNextTrack(isUserInitiated: Bool) { let assetCount = PlaylistManager.shared.numberOfAssets - let isAtEnd = currentlyPlayingItemIndex >= assetCount - 1 - var index = currentlyPlayingItemIndex + let isAtEnd = PlaylistCarplayManager.shared.currentlyPlayingItemIndex >= assetCount - 1 + var index = PlaylistCarplayManager.shared.currentlyPlayingItemIndex switch player.repeatState { case .none: @@ -262,12 +279,12 @@ extension PlaylistCarplayController { self.displayExpiredResourceError(item: item) } else { DispatchQueue.main.async { - self.currentlyPlayingItemIndex = index + PlaylistCarplayManager.shared.currentlyPlayingItemIndex = index self.onNextTrack(isUserInitiated: isUserInitiated) } } case .none: - self.currentlyPlayingItemIndex = index + PlaylistCarplayManager.shared.currentlyPlayingItemIndex = index self.updateLastPlayedItem(item: item) case .cancelled: log.debug("User Cancelled Playlist Playback") diff --git a/Client/Frontend/Browser/Playlist/Controller/PlaylistListViewController.swift b/Client/Frontend/Browser/Playlist/Controller/PlaylistListViewController.swift index 35dc92e052a..549efa269ac 100644 --- a/Client/Frontend/Browser/Playlist/Controller/PlaylistListViewController.swift +++ b/Client/Frontend/Browser/Playlist/Controller/PlaylistListViewController.swift @@ -106,12 +106,19 @@ class PlaylistListViewController: UIViewController { // After reloading all data, update the background guard PlaylistManager.shared.numberOfAssets > 0 else { self.updateTableBackgroundView() + self.autoPlayEnabled = true return } // Otherwise prepare to play the first item playerView.setControlsEnabled(true) + // If car play is active or media is already playing, do nothing + if PlaylistCarplayManager.shared.isCarPlayAvailable && (delegate?.currentPlaylistAsset != nil || delegate?.isPlaying ?? false) { + self.autoPlayEnabled = true + return + } + // If there is no last played item, then just select the first item in the playlist // which will play it if auto-play is enabled. guard let lastPlayedItemUrl = Preferences.Playlist.lastPlayedItemUrl.value, @@ -134,6 +141,8 @@ class PlaylistListViewController: UIViewController { } delegate.playItem(item: item) { [weak self] error in + PlaylistCarplayManager.shared.currentPlaylistItem = nil + guard let self = self, let delegate = self.delegate else { self?.commitPlayerItemTransaction(at: indexPath, @@ -155,6 +164,7 @@ class PlaylistListViewController: UIViewController { isExpired: true) delegate.displayExpiredResourceError(item: item) case .none: + PlaylistCarplayManager.shared.currentPlaylistItem = item self.commitPlayerItemTransaction(at: indexPath, isExpired: false) diff --git a/Client/Frontend/Browser/Playlist/Controller/PlaylistViewController.swift b/Client/Frontend/Browser/Playlist/Controller/PlaylistViewController.swift index f5f5e296003..377be3c2cb4 100644 --- a/Client/Frontend/Browser/Playlist/Controller/PlaylistViewController.swift +++ b/Client/Frontend/Browser/Playlist/Controller/PlaylistViewController.swift @@ -32,6 +32,7 @@ protocol PlaylistViewControllerDelegate: AnyObject { func displayLoadingResourceError() func displayExpiredResourceError(item: PlaylistInfo) + var isPlaying: Bool { get } var currentPlaylistItem: AVPlayerItem? { get } var currentPlaylistAsset: AVAsset? { get } } @@ -53,7 +54,6 @@ class PlaylistViewController: UIViewController { private var playerStateObservers = Set() private var assetStateObservers = Set() private var assetLoadingStateObservers = Set() - private var currentlyPlayingItemIndex = -1 init(player: MediaPlayer) { self.player = player @@ -78,7 +78,10 @@ class PlaylistViewController: UIViewController { player.pictureInPictureController?.stopPictureInPicture() // Stop media playback - stop(playerView) + if !PlaylistCarplayManager.shared.isCarPlayAvailable { + stop(playerView) + PlaylistCarplayManager.shared.currentPlaylistItem = nil + } // If this controller is retained in app-delegate for Picture-In-Picture support // then we need to re-attach the player layer @@ -98,6 +101,7 @@ class PlaylistViewController: UIViewController { // Setup delegates and state observers attachPlayerView() + updatePlayerUI() observePlayerStates() listController.delegate = self @@ -170,6 +174,43 @@ class PlaylistViewController: UIViewController { } } + private func updatePlayerUI() { + // Update play/pause button + if isPlaying { + playerView.controlsView.playPauseButton.setImage(#imageLiteral(resourceName: "playlist_pause"), for: .normal) + } else { + playerView.controlsView.playPauseButton.setImage(#imageLiteral(resourceName: "playlist_play"), for: .normal) + } + + // Update play-backrate button + let playbackRate = player.rate + let button = playerView.controlsView.playbackRateButton + + if playbackRate <= 1.0 { + button.setTitle("1x", for: .normal) + } else if playbackRate == 1.5 { + button.setTitle("1.5x", for: .normal) + } else { + button.setTitle("2x", for: .normal) + } + + // Update repeatMode button + switch repeatMode { + case .none: + playerView.controlsView.repeatButton.setImage(#imageLiteral(resourceName: "playlist_repeat"), for: .normal) + case .repeatOne: + playerView.controlsView.repeatButton.setImage(#imageLiteral(resourceName: "playlist_repeat_one"), for: .normal) + case .repeatAll: + playerView.controlsView.repeatButton.setImage(#imageLiteral(resourceName: "playlist_repeat_all"), for: .normal) + } + + if let item = PlaylistCarplayManager.shared.currentPlaylistItem { + playerView.setVideoInfo(videoDomain: item.pageSrc, videoTitle: item.pageTitle) + } else { + playerView.resetVideoInfo() + } + } + private func observePlayerStates() { player.publisher(for: .play).sink { [weak self] _ in self?.playerView.controlsView.playPauseButton.setImage(#imageLiteral(resourceName: "playlist_pause"), for: .normal) @@ -189,7 +230,7 @@ class PlaylistViewController: UIViewController { let playbackRate = self.player.rate let button = self.playerView.controlsView.playbackRateButton - if playbackRate == 1.0 { + if playbackRate <= 1.0 { button.setTitle("1x", for: .normal) } else if playbackRate == 1.5 { button.setTitle("1.5x", for: .normal) @@ -200,7 +241,6 @@ class PlaylistViewController: UIViewController { player.publisher(for: .changeRepeatMode).sink { [weak self] _ in guard let self = self else { return } - switch self.repeatMode { case .none: self.playerView.controlsView.repeatButton.setImage(#imageLiteral(resourceName: "playlist_repeat"), for: .normal) @@ -329,10 +369,10 @@ extension PlaylistViewController: PlaylistViewControllerDelegate { func deleteItem(item: PlaylistInfo, at index: Int) { PlaylistManager.shared.delete(item: item) - if currentlyPlayingItemIndex == index { + if PlaylistCarplayManager.shared.currentlyPlayingItemIndex == index { PlaylistMediaStreamer.clearNowPlayingInfo() - currentlyPlayingItemIndex = -1 + PlaylistCarplayManager.shared.currentlyPlayingItemIndex = -1 playerView.resetVideoInfo() stop(playerView) @@ -388,11 +428,11 @@ extension PlaylistViewController: VideoViewDelegate { } func onPreviousTrack(_ videoView: VideoView, isUserInitiated: Bool) { - if currentlyPlayingItemIndex <= 0 { + if PlaylistCarplayManager.shared.currentlyPlayingItemIndex <= 0 { return } - let index = currentlyPlayingItemIndex - 1 + let index = PlaylistCarplayManager.shared.currentlyPlayingItemIndex - 1 if index < PlaylistManager.shared.numberOfAssets { let indexPath = IndexPath(row: index, section: 0) listController.prepareToPlayItem(at: indexPath) { [weak self] item in @@ -403,7 +443,7 @@ extension PlaylistViewController: VideoViewDelegate { return } - self.currentlyPlayingItemIndex = indexPath.row + PlaylistCarplayManager.shared.currentlyPlayingItemIndex = indexPath.row self.playItem(item: item) { [weak self] error in guard let self = self else { return } @@ -417,7 +457,7 @@ extension PlaylistViewController: VideoViewDelegate { self.displayExpiredResourceError(item: item) case .none: self.listController.commitPlayerItemTransaction(at: indexPath, isExpired: false) - self.currentlyPlayingItemIndex = index + PlaylistCarplayManager.shared.currentlyPlayingItemIndex = index self.updateLastPlayedItem(item: item) case .cancelled: self.listController.commitPlayerItemTransaction(at: indexPath, isExpired: false) @@ -430,8 +470,8 @@ extension PlaylistViewController: VideoViewDelegate { func onNextTrack(_ videoView: VideoView, isUserInitiated: Bool) { let assetCount = PlaylistManager.shared.numberOfAssets - let isAtEnd = currentlyPlayingItemIndex >= assetCount - 1 - var index = currentlyPlayingItemIndex + let isAtEnd = PlaylistCarplayManager.shared.currentlyPlayingItemIndex >= assetCount - 1 + var index = PlaylistCarplayManager.shared.currentlyPlayingItemIndex switch repeatMode { case .none: @@ -482,13 +522,13 @@ extension PlaylistViewController: VideoViewDelegate { } else { DispatchQueue.main.async { self.listController.commitPlayerItemTransaction(at: indexPath, isExpired: false) - self.currentlyPlayingItemIndex = index + PlaylistCarplayManager.shared.currentlyPlayingItemIndex = index self.onNextTrack(videoView, isUserInitiated: isUserInitiated) } } case .none: self.listController.commitPlayerItemTransaction(at: indexPath, isExpired: false) - self.currentlyPlayingItemIndex = index + PlaylistCarplayManager.shared.currentlyPlayingItemIndex = index self.updateLastPlayedItem(item: item) case .cancelled: self.listController.commitPlayerItemTransaction(at: indexPath, isExpired: false) diff --git a/Client/Frontend/Browser/Playlist/PlaylistCarplayManager.swift b/Client/Frontend/Browser/Playlist/PlaylistCarplayManager.swift index 378a28a4d4d..025735b1204 100644 --- a/Client/Frontend/Browser/Playlist/PlaylistCarplayManager.swift +++ b/Client/Frontend/Browser/Playlist/PlaylistCarplayManager.swift @@ -6,15 +6,22 @@ import Foundation import Combine import MediaPlayer +import Shared +import Data + +private let log = Logger.browserLogger /// Lightweight class that manages a single MediaPlayer item /// The MediaPlayer is then passed to any controller that needs to use it. class PlaylistCarplayManager: NSObject { - private var carPlayStatusObservers = Set() - private let contentManager = MPPlayableContentManager.shared() - private let carPlayStatus = CurrentValueSubject(false) + private var carPlayStatusObservers = [Any]() + private var contentManager = MPPlayableContentManager.shared() private var carPlayController: PlaylistCarplayController? private weak var mediaPlayer: MediaPlayer? + private(set) var isCarPlayAvailable: Bool = false + + var currentlyPlayingItemIndex: Int = -1 + var currentPlaylistItem: PlaylistInfo? // There can only ever be one instance of this class // Because there can only be a single AudioSession and MediaPlayer @@ -30,20 +37,30 @@ class PlaylistCarplayManager: NSObject { // That way, we can determine where the controls are coming from for Playlist // OR determine where the AudioSession is outputting - AVAudioSession.sharedInstance().currentRoute.outputs.publisher.contains(where: { $0.portType == .carAudio }).sink { [weak self] isCarPlayAvailable in - self?.attemptInterfaceConnection(isCarPlayAvailable: isCarPlayAvailable) - }.store(in: &carPlayStatusObservers) + // We need to observe the audio route because sometimes the car will be disconnected + // and contentManager.context.endpointAvailable will still return true! + carPlayStatusObservers.append(NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: nil, queue: .main) { [weak self] _ in + + let hasCarPlay = AVAudioSession.sharedInstance().currentRoute.outputs.contains(where: { $0.portType == .carAudio }) + self?.attemptInterfaceConnection(isCarPlayAvailable: hasCarPlay) + }) - UIDevice.current.publisher(for: \.userInterfaceIdiom).map({ $0 == .carPlay }).sink { [weak self] isCarPlayAvailable in - self?.attemptInterfaceConnection(isCarPlayAvailable: isCarPlayAvailable) - }.store(in: &carPlayStatusObservers) + // Using publisher for this crashes no matter what! + // The moment you call `sink` on the publisher, it will crash. + // Seems like a bug in iOS itself. + // We observe the contentManager.context.endpointAvailable to determine when to create + // a carplay handler + carPlayStatusObservers.append(contentManager.observe(\.context) { [weak self] contentManager, _ in + self?.carPlayStatusObservers.append(contentManager.context.observe(\.endpointAvailable) { [weak self] context, change in + self?.attemptInterfaceConnection(isCarPlayAvailable: context.endpointAvailable) + }) + }) - carPlayController = getCarPlayController() - } - - deinit { - carPlayController = nil - mediaPlayer = nil + // This is needed because the notifications for carplay doesn't get posted initial + // until you actually attempt to use the AudioSession or Context + let hasCarPlay = AVAudioSession.sharedInstance().currentRoute.outputs.contains(where: { $0.portType == .carAudio }) + let hasCarPlayEndpoint = contentManager.context.endpointAvailable + attemptInterfaceConnection(isCarPlayAvailable: hasCarPlay || hasCarPlayEndpoint) } func getCarPlayController() -> PlaylistCarplayController { @@ -64,25 +81,23 @@ class PlaylistCarplayManager: NSObject { return playlistController } - func attemptInterfaceConnection(isCarPlayAvailable: Bool) { + private func attemptInterfaceConnection(isCarPlayAvailable: Bool) { + self.isCarPlayAvailable = isCarPlayAvailable + // If there is no media player, create one, // pass it to the carplay controller -// if isCarPlayAvailable { -// // Protect against reentrancy. -// if self.carPlayController == nil { -// self.carPlayController = self.getCarPlayController() -// } -// } else { -// self.carPlayController = nil -// } -// -// self.carPlayStatus.send(isCarPlayAvailable) -// print("CARPLAY CONNECTED: \(isCarPlayAvailable)") - } -} + if isCarPlayAvailable { + // Protect against reentrancy. + if carPlayController == nil { + carPlayController = self.getCarPlayController() + } + } else { + carPlayController = nil + mediaPlayer = nil + } -extension PlaylistCarplayManager: MPPlayableContentDelegate { - func playableContentManager(_ contentManager: MPPlayableContentManager, didUpdate context: MPPlayableContentManagerContext) { - attemptInterfaceConnection(isCarPlayAvailable: context.endpointAvailable) + // Sometimes the `endpointAvailable` WILL RETURN TRUE! + // Even when the car is NOT connected. + log.debug("CARPLAY CONNECTED: \(isCarPlayAvailable) -- \(contentManager.context.endpointAvailable)") } } diff --git a/Client/Frontend/Browser/Playlist/Utilities/PlaylistMediaStreamer.swift b/Client/Frontend/Browser/Playlist/Utilities/PlaylistMediaStreamer.swift index c79f311df25..d7d082d9dec 100644 --- a/Client/Frontend/Browser/Playlist/Utilities/PlaylistMediaStreamer.swift +++ b/Client/Frontend/Browser/Playlist/Utilities/PlaylistMediaStreamer.swift @@ -124,11 +124,10 @@ class PlaylistMediaStreamer { MPMediaItemPropertyTitle: item.name, MPMediaItemPropertyArtist: URL(string: item.pageSrc)?.baseDomain ?? item.pageSrc, MPMediaItemPropertyPlaybackDuration: item.duration, - MPNowPlayingInfoPropertyPlaybackRate: player.rate, MPNowPlayingInfoPropertyPlaybackProgress: 0.0, - MPNowPlayingInfoPropertyDefaultPlaybackRate: 1.0, +// MPNowPlayingInfoPropertyDefaultPlaybackRate: 1.0, MPNowPlayingInfoPropertyAssetURL: URL(string: item.pageSrc) as Any, - MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime.seconds + MPNowPlayingInfoPropertyElapsedPlaybackTime: 0.0, //player.currentTime.seconds ] } @@ -138,11 +137,18 @@ class PlaylistMediaStreamer { static func setNowPlayingMediaArtwork(image: UIImage?) { if let image = image { - MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { _ -> UIImage in + let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { _ -> UIImage in // Do not resize image here. // According to Apple it isn't necessary to use expensive resize operations return image }) + setNowPlayingMediaArtwork(artwork: artwork) + } + } + + static func setNowPlayingMediaArtwork(artwork: MPMediaItemArtwork?) { + if let artwork = artwork { + MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyArtwork] = artwork } } diff --git a/Client/Frontend/Browser/Playlist/Utilities/PlaylistThumbnailUtility.swift b/Client/Frontend/Browser/Playlist/Utilities/PlaylistThumbnailUtility.swift index 08265c5c360..f30d5c01a96 100644 --- a/Client/Frontend/Browser/Playlist/Utilities/PlaylistThumbnailUtility.swift +++ b/Client/Frontend/Browser/Playlist/Utilities/PlaylistThumbnailUtility.swift @@ -35,14 +35,14 @@ public class PlaylistThumbnailRenderer { }.eraseToAnyPublisher() } - chainedGenerator.receive(on: RunLoop.main).sink { + chainedGenerator.receive(on: RunLoop.main).sink(receiveCompletion: { if case .failure(let error) = $0 { log.error(error) completion(nil) } - } receiveValue: { + }, receiveValue: { completion($0) - }.store(in: &thumbnailGenerator) + }).store(in: &thumbnailGenerator) } } diff --git a/Client/Frontend/Browser/Playlist/VideoPlayer/MediaPlayer.swift b/Client/Frontend/Browser/Playlist/VideoPlayer/MediaPlayer.swift index a13e805c0bd..e5605ab9e75 100644 --- a/Client/Frontend/Browser/Playlist/VideoPlayer/MediaPlayer.swift +++ b/Client/Frontend/Browser/Playlist/VideoPlayer/MediaPlayer.swift @@ -27,6 +27,7 @@ class MediaPlayer: NSObject { private(set) public var pendingMediaItem: AVPlayerItem? private(set) public var pictureInPictureController: AVPictureInPictureController? private(set) var repeatState: RepeatMode = .none + private(set) var previousRate: Float = 0.0 public var isPlaying: Bool { // It is better NOT to keep tracking of isPlaying OR rate > 0.0 @@ -149,20 +150,23 @@ class MediaPlayer: NSObject { func play() { if !isPlaying { - playSubscriber.send(EventNotification(mediaPlayer: self, event: .play)) player.play() + player.rate = previousRate + playSubscriber.send(EventNotification(mediaPlayer: self, event: .play)) } } func pause() { if isPlaying { - pauseSubscriber.send(EventNotification(mediaPlayer: self, event: .pause)) + previousRate = player.rate player.pause() + pauseSubscriber.send(EventNotification(mediaPlayer: self, event: .pause)) } } func stop() { if isPlaying { + previousRate = 0.0 player.pause() player.replaceCurrentItem(with: nil) stopSubscriber.send(EventNotification(mediaPlayer: self, event: .stop)) @@ -262,6 +266,7 @@ class MediaPlayer: NSObject { } func setPlaybackRate(rate: Float) { + previousRate = player.rate player.rate = rate changePlaybackRateSubscriber.send(EventNotification(mediaPlayer: self, event: .changePlaybackRate))