Last active
June 6, 2022 02:45
-
-
Save FlorianHardyDev/3cf5863aa83e26d493edfe6fcb48ab24 to your computer and use it in GitHub Desktop.
Podcast Player
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import AVKit | |
| import MediaPlayer | |
| import SDWebImage | |
| import UIKit | |
| // This class handle audio for podcast and videos inside the all application | |
| class PlayerManager: NSObject { | |
| // Audio components | |
| var videoPlayer = AVPlayer() | |
| var podcastPlayer = AVPlayer() | |
| var modifiedRate: Float = 1 | |
| private var playbackLikelyToKeepUpContext = 0 | |
| private var podcastNowPlayingInfo = [String: Any]() | |
| struct Observation { | |
| weak var observer: PodcastPlayerObserver? | |
| } | |
| private var observations = [ObjectIdentifier: Observation]() | |
| // View components | |
| var bigPlayerWindow: UIWindow? | |
| var playerDetailViewController: PlayerDetailsViewController? | |
| var floatingButtonController: FloatingButtonController? | |
| var smallPlayerLastLocation: CGPoint? | |
| private var isAVAudioSessionActive = false | |
| // Data informations | |
| private var playlist: Playlist? | |
| private var currentIndex: Int = 0 | |
| private var state = State.paused(nil) { | |
| // We add a property observer on 'state', which lets us | |
| // run a function on each value change. | |
| didSet { stateDidChange() } | |
| } | |
| // MARK: - Lifecycle | |
| deinit { | |
| self.videoPlayer.removeObserver(self, forKeyPath: "rate") | |
| self.podcastPlayer.removeObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp") | |
| NotificationCenter.default.removeObserver(self) | |
| } | |
| // MARK: - Singleton | |
| static var shared: PlayerManager = { | |
| let playerManager = PlayerManager() | |
| do { | |
| // This is necessary if you want to have audio in the app ! | |
| try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback) | |
| } catch { | |
| Logger.log(.error, "Unable to set AVAudioSession Category: \(error)") | |
| } | |
| playerManager.podcastPlayer = AVPlayer() | |
| playerManager.podcastPlayer.automaticallyWaitsToMinimizeStalling = false | |
| playerManager.videoPlayer = AVPlayer() | |
| playerManager.setupVideoPlayer() | |
| return playerManager | |
| }() | |
| } | |
| // MARK: - Configuration | |
| extension PlayerManager { | |
| // MARK: Configuration (Audio) | |
| private func setupVideoPlayer() { | |
| self.videoPlayer.addObserver(self, forKeyPath: "rate", options: NSKeyValueObservingOptions.new, context: nil) | |
| } | |
| private func setupAVAudioSession() { | |
| if !self.isAVAudioSessionActive { | |
| do { | |
| try AVAudioSession.sharedInstance().setActive(true) | |
| NotificationCenter.default.addObserver(self, | |
| selector: #selector(handleInterruption(notification:)), | |
| name: AVAudioSession.interruptionNotification, | |
| object: nil) | |
| NotificationCenter.default.addObserver(self, | |
| selector: #selector(handleSecondaryAudio), | |
| name: AVAudioSession.silenceSecondaryAudioHintNotification, | |
| object: AVAudioSession.sharedInstance()) | |
| Logger.log(.info, "AVAudioSession is Active and Category Playback is set") | |
| UIApplication.shared.beginReceivingRemoteControlEvents() | |
| self.setupCommandCenter() | |
| self.isAVAudioSessionActive = true | |
| } catch { | |
| Logger.log(.error, "Unable to activate audio session: \(error.localizedDescription)") | |
| } | |
| } | |
| } | |
| private func removeAVAudioSession() { | |
| if self.isAVAudioSessionActive { | |
| do { | |
| try AVAudioSession.sharedInstance().setActive(false) | |
| Logger.log(.info, "AVAudioSession is deactivate") | |
| UIApplication.shared.endReceivingRemoteControlEvents() | |
| self.removeCommandCenter() | |
| self.isAVAudioSessionActive = false | |
| } catch { | |
| Logger.log(.error, "Unable to deactivate audio session: \(error)") | |
| } | |
| } | |
| } | |
| // MARK: Configuration (View) | |
| private func setupSmallPlayer(with style: UIStatusBarStyle) { | |
| if floatingButtonController == nil { | |
| floatingButtonController = FloatingButtonController() | |
| floatingButtonController?.floatingButtonDelegate = self | |
| floatingButtonController?.statusBarStyle = style | |
| floatingButtonController?.showWindow() | |
| } | |
| } | |
| private func setupBigPlayer() { | |
| guard let playlistSafe = self.playlist else { | |
| return | |
| } | |
| self.playerDetailViewController = PlayerDetailsViewController(with: playlistSafe.audios[currentIndex], | |
| from: playlistSafe) | |
| if bigPlayerWindow == nil { | |
| let statusBarHeight = UIApplication.shared.statusBarFrame.height | |
| bigPlayerWindow = UIWindow(frame: CGRect(x: 0.0, | |
| y: statusBarHeight, | |
| width: UIScreen.main.bounds.width, | |
| height: UIScreen.main.bounds.height - statusBarHeight)) | |
| bigPlayerWindow?.addShadow(shadowColor: .black, | |
| offSet: CGSize(width: 2.6, height: 2.6), | |
| opacity: 0.8, | |
| shadowRadius: 5.0, | |
| cornerRadius: PlayerDetailsViewController.Constants.cornerRadius, | |
| corners: [UIRectCorner.topLeft, UIRectCorner.topRight]) | |
| bigPlayerWindow?.rootViewController = self.playerDetailViewController | |
| bigPlayerWindow?.makeKeyAndVisible() | |
| bigPlayerWindow?.windowLevel = .normal | |
| bigPlayerWindow?.layer.zPosition = .infinity | |
| bigPlayerWindow?.isHidden = true | |
| setupBigPlayerSwipeGestures() | |
| } else { | |
| bigPlayerWindow?.rootViewController = self.playerDetailViewController | |
| } | |
| } | |
| private func setupBigPlayerSwipeGestures() { | |
| let swipeDown = UISwipeGestureRecognizer(target: self, | |
| action: #selector(handleGestureOnBigPlayer)) | |
| swipeDown.direction = .down | |
| bigPlayerWindow?.addGestureRecognizer(swipeDown) | |
| } | |
| } | |
| // MARK: - Podcast loading | |
| extension PlayerManager { | |
| func launchPodcastPlaylist(with style: UIStatusBarStyle, | |
| playlist: Playlist) { | |
| self.podcastPlayer.automaticallyWaitsToMinimizeStalling = false | |
| self.playlist = playlist | |
| self.currentIndex = playlist.currentIndex | |
| self.setupSmallPlayer(with: style) | |
| self.setupAVAudioSession() | |
| guard let playlistSafe = self.playlist, | |
| let audioSafe = playlistSafe.audioToPlay else { | |
| return | |
| } | |
| self.loadPodcast(audio: audioSafe) | |
| } | |
| private func loadPodcast(audio: Audio, andPlay shouldPlay: Bool = true) { | |
| guard let stringUrl = audio.url, let safeUrl = URL(string: stringUrl) else { | |
| return | |
| } | |
| self.podcastPlayer = AVPlayer(playerItem: AVPlayerItem(url: safeUrl)) | |
| // Register for notification | |
| NotificationCenter.default.addObserver(self, | |
| selector: #selector(playerItemDidReadyToPlay(notification:)), | |
| name: .AVPlayerItemNewAccessLogEntry, | |
| object: self.podcastPlayer.currentItem) | |
| self.podcastPlayer.addObserver(self, | |
| forKeyPath: "currentItem.playbackLikelyToKeepUp", | |
| options: .new, | |
| context: &playbackLikelyToKeepUpContext) | |
| NotificationCenter.default.addObserver(self, | |
| selector: #selector(playerItemDidReachEnd), | |
| name: .AVPlayerItemDidPlayToEndTime, | |
| object: self.podcastPlayer.currentItem) | |
| self.updateInfoCenter(with: audio) | |
| self.updateCommandCenter() | |
| if shouldPlay { | |
| self.playPodcast(freshlyLoaded: true) | |
| } | |
| // Setup the big player after the AVPlayer because it is required inside | |
| self.setupBigPlayer() | |
| } | |
| } | |
| // MARK: - Podcast controls | |
| extension PlayerManager { | |
| func playPodcast(freshlyLoaded: Bool = false) { | |
| videoPlayer.pause() | |
| if !isAVAudioSessionActive { | |
| self.setupAVAudioSession() | |
| } | |
| if self.podcastPlayer.currentItem == nil { | |
| if let playlistSafe = self.playlist { | |
| self.loadPodcast(audio: playlistSafe.audios[currentIndex]) | |
| } | |
| } | |
| if freshlyLoaded { | |
| self.podcastPlayer.playImmediately(atRate: 1.0) | |
| self.modifiedRate = 1.0 | |
| } else { | |
| self.podcastPlayer.playImmediately(atRate: modifiedRate) | |
| } | |
| state = .playing(podcastPlayer.currentItem) | |
| self.playerDetailViewController?.setPlayButton() | |
| } | |
| func pausePodcast() { | |
| self.podcastPlayer.pause() | |
| state = .paused(podcastPlayer.currentItem) | |
| } | |
| private func stopPodcast() { | |
| self.podcastPlayer.seek(to: CMTime.zero) | |
| self.pausePodcast() | |
| } | |
| } | |
| // MARK: - Video loading | |
| extension PlayerManager { | |
| func loadVideo(url: URL) { | |
| self.pausePodcastAndRemoveAudioSession() | |
| self.videoPlayer.replaceCurrentItem(with: AVPlayerItem(url: url)) | |
| } | |
| } | |
| // MARK: - Observers adding/removing | |
| extension PlayerManager { | |
| func addObserver(_ observer: PodcastPlayerObserver) { | |
| let identifier = ObjectIdentifier(observer) | |
| observations[identifier] = Observation(observer: observer) | |
| } | |
| func removeObserver(_ observer: PodcastPlayerObserver) { | |
| let identifier = ObjectIdentifier(observer) | |
| observations.removeValue(forKey: identifier) | |
| } | |
| } | |
| // MARK: - Observers | |
| extension PlayerManager { | |
| override func observeValue(forKeyPath keyPath: String?, | |
| of object: Any?, | |
| change: [NSKeyValueChangeKey: Any]?, | |
| context: UnsafeMutableRawPointer?) { | |
| if context == &playbackLikelyToKeepUpContext { | |
| guard let safeItem = self.podcastPlayer.currentItem else { | |
| return | |
| } | |
| self.state = .loading(safeItem, !safeItem.isPlaybackLikelyToKeepUp) | |
| if safeItem.isPlaybackLikelyToKeepUp && isPodcastPlaying() { | |
| self.floatingButtonController?.playAnimation() | |
| } else { | |
| self.floatingButtonController?.stopAnimation() | |
| } | |
| } | |
| // Observe the video player | |
| if keyPath == "rate" && videoPlayer.error == nil { | |
| // When the video player is played | |
| if self.videoPlayer.rate != 0.0 { | |
| if self.isAVAudioSessionActive { | |
| // The audio needs to be paused or stopped to remove audio session | |
| self.videoPlayer.pause() | |
| self.pausePodcastAndRemoveAudioSession() | |
| // After removing audio session, the audio can be played | |
| self.videoPlayer.play() | |
| } | |
| } else if self.isPodcastPlaying() { | |
| self.setupAVAudioSession() | |
| } | |
| } | |
| } | |
| @objc | |
| func playerItemDidReachEnd(notification: NSNotification) { | |
| if isNextPodcastAvailable() { | |
| self.podcastNextPressed() | |
| } else { | |
| self.pausePodcast() | |
| if let playlistSafe = self.playlist, | |
| self.currentIndex == playlistSafe.audios.count - 1, | |
| let audioSafe = playlistSafe.audios.first { | |
| self.currentIndex = 0 | |
| self.loadPodcast(audio: audioSafe, andPlay: false) | |
| self.podcastPlayer.seek(to: CMTime.zero) | |
| } else { | |
| self.podcastPlayer.seek(to: CMTime.zero) | |
| } | |
| } | |
| } | |
| @objc | |
| func playerItemDidReadyToPlay(notification: Notification) { | |
| if let item = notification.object as? AVPlayerItem { | |
| if item.status == .readyToPlay { | |
| self.updateNowPlayingInfo() | |
| self.updateCommandCenter() | |
| self.playerDetailViewController?.setSlider(with: item) | |
| } else if item.status == .failed { | |
| Logger.log(.error, "something went wrong. player.error should contain some information") | |
| } | |
| } | |
| } | |
| @objc | |
| func handleSecondaryAudio(notification: Notification) { | |
| // Determine hint type | |
| guard let userInfo = notification.userInfo, | |
| let typeValue = userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey] as? UInt, | |
| let type = AVAudioSession.SilenceSecondaryAudioHintType(rawValue: typeValue) else { | |
| return | |
| } | |
| if type == .begin { | |
| // Other app audio started playing - mute secondary audio | |
| self.pausePodcast() | |
| } else { | |
| // Other app audio stopped playing - restart secondary audio | |
| self.playPodcast() | |
| } | |
| } | |
| @objc | |
| func handleInterruption(notification: Notification) { | |
| guard let userInfo = notification.userInfo, | |
| let typeInt = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, | |
| let type = AVAudioSession.InterruptionType(rawValue: typeInt) else { | |
| return | |
| } | |
| switch type { | |
| case .began: | |
| self.pausePodcast() | |
| case .ended: | |
| if let optionInt = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt { | |
| let options = AVAudioSession.InterruptionOptions(rawValue: optionInt) | |
| if options.contains(.shouldResume) { | |
| self.playPodcast() | |
| } | |
| } | |
| // do nothing (here if other case are added in future versions) | |
| @unknown default: break | |
| } | |
| } | |
| } | |
| // MARK: - Getters | |
| extension PlayerManager { | |
| func getCurrentPlaylist() -> Playlist? { | |
| return self.playlist | |
| } | |
| func getCurrentPodcastUrl() -> String? { | |
| return (self.podcastPlayer.currentItem?.asset as? AVURLAsset)?.url.absoluteString | |
| } | |
| func isPodcastPlaying() -> Bool { | |
| return self.podcastPlayer.rate != 0.0 | |
| } | |
| func isNextPodcastAvailable() -> Bool { | |
| guard let playlistSafe = self.playlist else { | |
| return false | |
| } | |
| return self.currentIndex < playlistSafe.audios.count - 1 | |
| } | |
| func isVideoPlayerInFullscreenMode() -> Bool { | |
| guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, | |
| let controller = appDelegate.topViewController(with: appDelegate.window?.rootViewController), | |
| let aClass = NSClassFromString("AVFullScreenViewController"), | |
| controller.isKind(of: aClass) else { | |
| return false | |
| } | |
| return true | |
| } | |
| } | |
| // MARK: - Setters | |
| extension PlayerManager { | |
| func setPreferedStatusBarStyle(with barStyle: UIStatusBarStyle) { | |
| guard let fab = floatingButtonController else { | |
| return | |
| } | |
| fab.statusBarStyle = barStyle | |
| fab.setNeedsStatusBarAppearanceUpdate() | |
| } | |
| // This method is called only when app is coming from background ! | |
| func updateMiniPlayerAnimation() { | |
| let keepUp = self.podcastPlayer.currentItem?.isPlaybackLikelyToKeepUp ?? false | |
| self.isPodcastPlaying() && keepUp | |
| ? self.floatingButtonController?.playAnimation() | |
| : self.floatingButtonController?.stopAnimation() | |
| } | |
| } | |
| // MARK: - Actions | |
| extension PlayerManager { | |
| @objc | |
| private func handleGestureOnBigPlayer(gesture: UISwipeGestureRecognizer) { | |
| if gesture.direction == .down { | |
| hidePlayerDetails() | |
| } | |
| } | |
| @objc | |
| func hidePlayerDetails() { | |
| bigPlayerWindow?.isHidden = true | |
| floatingButtonController?.showWindow() | |
| } | |
| private func showPlayerDetails() { | |
| self.floatingButtonController?.removeWindow() | |
| self.bigPlayerWindow?.isHidden = false | |
| } | |
| func enterFullScreen() { | |
| self.pausePodcast() | |
| self.floatingButtonController?.removeWindow() | |
| self.bigPlayerWindow?.isHidden = true | |
| } | |
| func exitFullScreen() { | |
| self.floatingButtonController?.showWindow() | |
| } | |
| func podcastGoTo(time: Float) { | |
| self.podcastPlayer.seek(to: CMTime(seconds: Double(time), | |
| preferredTimescale: self.podcastPlayer.currentTime().timescale)) {_ in | |
| self.updateNowPlayingInfo() | |
| } | |
| } | |
| private func podcastSeekTo(time: CMTime) { | |
| self.podcastPlayer.seek(to: time) {_ in | |
| self.updateNowPlayingInfo() | |
| } | |
| } | |
| func podcastBackwardPressed() { | |
| let newTime = CMTimeGetSeconds(self.podcastPlayer.currentTime()) - 10 | |
| self.podcastSeekTo(time: CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)) | |
| } | |
| func podcastPreviousPressed() { | |
| let newCurrentIndex = currentIndex - 1 | |
| if newCurrentIndex < 0 { | |
| self.podcastSeekTo(time: CMTime(seconds: 0, | |
| preferredTimescale: self.podcastPlayer.currentTime().timescale)) | |
| } else { | |
| guard let playlistSafe = self.playlist else { | |
| return | |
| } | |
| self.currentIndex = newCurrentIndex | |
| self.loadPodcast(audio: playlistSafe.audios[newCurrentIndex]) | |
| } | |
| } | |
| func podcastPlayPressed() { | |
| isPodcastPlaying() ? pausePodcast() : playPodcast() | |
| } | |
| func podcastNextPressed() { | |
| let newCurrentIndex = currentIndex + 1 | |
| guard let playlistSafe = self.playlist, | |
| newCurrentIndex <= playlistSafe.audios.count - 1 else { | |
| return | |
| } | |
| self.currentIndex = newCurrentIndex | |
| self.loadPodcast(audio: playlistSafe.audios[newCurrentIndex]) | |
| } | |
| func podcastForwardPressed() { | |
| let newTime = CMTimeGetSeconds(self.podcastPlayer.currentTime()) + 10 | |
| self.podcastSeekTo(time: CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)) | |
| } | |
| @discardableResult | |
| func changeRate() -> Float? { | |
| // Calculate the new rate | |
| self.modifiedRate += 0.25 | |
| if self.modifiedRate > 2 { | |
| self.modifiedRate = 1 | |
| } | |
| // If the podcast player was playing | |
| if self.podcastPlayer.rate != 0 { | |
| // Change the rate directly | |
| self.podcastPlayer.rate = self.modifiedRate | |
| } | |
| // If it's not playing, the rate will be changed at the next playPoscast() | |
| return self.modifiedRate | |
| } | |
| private func pausePodcastAndRemoveAudioSession() { | |
| self.pausePodcast() | |
| if self.isAVAudioSessionActive { | |
| self.clearNowPlayingInfo() | |
| self.removeAVAudioSession() | |
| } | |
| } | |
| } | |
| // MARK: - Notification | |
| extension PlayerManager { | |
| private func getImage(from url: URL, completion: @escaping (UIImage?) -> Void) { | |
| URLSession.shared.dataTask(with: url, completionHandler: {data, _, _ in | |
| if let data = data { | |
| var image = UIImage(data: data) | |
| if let imageSafe = image { | |
| image = imageSafe.cropToBounds(image: imageSafe) | |
| } | |
| completion(image) | |
| } | |
| }).resume() | |
| } | |
| private func setupNowPlayingInfo(with artwork: MPMediaItemArtwork, for audio: Audio) { | |
| guard let safeCurrentItem = self.podcastPlayer.currentItem else { | |
| return | |
| } | |
| self.podcastNowPlayingInfo = [ | |
| MPMediaItemPropertyTitle: audio.title, | |
| MPMediaItemPropertyArtist: "", | |
| MPMediaItemPropertyAlbumTitle: "", | |
| MPMediaItemPropertyArtwork: artwork, | |
| MPNowPlayingInfoPropertyPlaybackRate: self.podcastPlayer.rate, | |
| MPMediaItemPropertyPlaybackDuration: CMTimeGetSeconds(safeCurrentItem.duration), | |
| MPNowPlayingInfoPropertyElapsedPlaybackTime: CMTimeGetSeconds(safeCurrentItem.currentTime()) | |
| ] | |
| // Set the metadata | |
| MPNowPlayingInfoCenter.default().nowPlayingInfo = self.podcastNowPlayingInfo | |
| } | |
| private func updateNowPlayingInfo() { | |
| guard let safeCurrentItem = self.podcastPlayer.currentItem else { | |
| return | |
| } | |
| self.podcastNowPlayingInfo.updateValue(self.podcastPlayer.rate, | |
| forKey: MPNowPlayingInfoPropertyPlaybackRate) | |
| self.podcastNowPlayingInfo.updateValue(CMTimeGetSeconds(safeCurrentItem.duration), | |
| forKey: MPMediaItemPropertyPlaybackDuration) | |
| self.podcastNowPlayingInfo.updateValue(CMTimeGetSeconds(self.podcastPlayer.currentTime()), | |
| forKey: MPNowPlayingInfoPropertyElapsedPlaybackTime) | |
| MPNowPlayingInfoCenter.default().nowPlayingInfo = self.podcastNowPlayingInfo | |
| } | |
| private func clearNowPlayingInfo() { | |
| MPNowPlayingInfoCenter.default().nowPlayingInfo = nil | |
| } | |
| private func updateInfoCenter(with audio: Audio) { | |
| guard let url = URL(string: audio.picture.url) else { return } | |
| getImage(from: url) { [weak self] image in | |
| guard let self = self, | |
| let downloadedImage = image else { | |
| return | |
| } | |
| let artwork = MPMediaItemArtwork(boundsSize: downloadedImage.size, requestHandler: { _ -> UIImage in | |
| return downloadedImage | |
| }) | |
| self.setupNowPlayingInfo(with: artwork, for: audio) | |
| } | |
| } | |
| } | |
| // MARK: - Command center | |
| extension PlayerManager { | |
| private func updateCommandCenter() { | |
| MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = isNextPodcastAvailable() | |
| } | |
| private func setupCommandCenter() { | |
| MPRemoteCommandCenter.shared().playCommand.isEnabled = true | |
| MPRemoteCommandCenter.shared().pauseCommand.isEnabled = true | |
| MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = true | |
| MPRemoteCommandCenter.shared().playCommand.addTarget { [weak self] _ -> MPRemoteCommandHandlerStatus in | |
| if self?.podcastPlayer.rate == 0.0 { | |
| self?.playPodcast() | |
| return .success | |
| } | |
| return .commandFailed | |
| } | |
| MPRemoteCommandCenter.shared().pauseCommand.addTarget { [weak self] _ -> MPRemoteCommandHandlerStatus in | |
| if self?.podcastPlayer.rate != 0.0 { | |
| self?.pausePodcast() | |
| return .success | |
| } | |
| return .commandFailed | |
| } | |
| MPRemoteCommandCenter.shared().previousTrackCommand.addTarget { [weak self] _ -> MPRemoteCommandHandlerStatus in | |
| self?.podcastPreviousPressed() | |
| return .success | |
| } | |
| MPRemoteCommandCenter.shared().nextTrackCommand.addTarget { [weak self] _ -> MPRemoteCommandHandlerStatus in | |
| self?.podcastNextPressed() | |
| return .success | |
| } | |
| } | |
| private func removeCommandCenter() { | |
| MPRemoteCommandCenter.shared().playCommand.isEnabled = false | |
| MPRemoteCommandCenter.shared().pauseCommand.isEnabled = false | |
| MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = false | |
| MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = false | |
| MPRemoteCommandCenter.shared().playCommand.removeTarget(nil) | |
| MPRemoteCommandCenter.shared().pauseCommand.removeTarget(nil) | |
| MPRemoteCommandCenter.shared().previousTrackCommand.removeTarget(nil) | |
| MPRemoteCommandCenter.shared().nextTrackCommand.removeTarget(nil) | |
| } | |
| } | |
| // MARK: - FloatingButtonDelegate extension | |
| extension PlayerManager: FloatingButtonDelegate { | |
| func closePodcastPlayer() { | |
| if self.podcastPlayer.currentItem != nil { | |
| TagManager.shared.tagEvent(.eventTagPlayerQuitAction, attributes: [ | |
| .tagAttributeFileName: self.getCurrentPodcastUrl() ?? "" | |
| ]) | |
| } | |
| self.stopPodcast() | |
| self.podcastPlayer.replaceCurrentItem(with: nil) | |
| self.floatingButtonController?.removeWindow() | |
| self.floatingButtonController = nil | |
| self.bigPlayerWindow = nil | |
| self.playerDetailViewController = nil | |
| self.removeAVAudioSession() | |
| } | |
| func onSmallPlayerTapped() { | |
| self.showPlayerDetails() | |
| } | |
| } | |
| // MARK: - AVPlayerViewControllerDelegate extension | |
| extension PlayerManager: AVPlayerViewControllerDelegate { | |
| // swiftlint:disable line_length | |
| func playerViewController(_ playerViewController: AVPlayerViewController, | |
| willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { | |
| self.enterFullScreen() | |
| } | |
| func playerViewController(_ playerViewController: AVPlayerViewController, | |
| willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { | |
| // Full screen video can be dismiss with a swipe gesture, | |
| // so you need to check if the user ended the gesture | |
| // to know if it is really dismissed | |
| coordinator.animate(alongsideTransition: nil) { context in | |
| if !context.isCancelled { | |
| self.exitFullScreen() | |
| } | |
| } | |
| } | |
| // swiftlint:enable line_length | |
| } | |
| private extension PlayerManager { | |
| func stateDidChange() { | |
| for (identifier, observation) in observations { | |
| // If the observer is no longer in memory, we | |
| // can clean up the observation for its ID | |
| guard let observer = observation.observer else { | |
| observations.removeValue(forKey: identifier) | |
| continue | |
| } | |
| switch state { | |
| case .playing(let item): | |
| observer.podcastPlayer(self.podcastPlayer, didStartPlaying: item) | |
| case .paused(let item): | |
| observer.podcastPlayer(self.podcastPlayer, didPausePlaybackOf: item) | |
| case let .loading(item, isLoading): | |
| observer.podcastPlayer(self.podcastPlayer, isLoading: isLoading, item: item) | |
| } | |
| } | |
| } | |
| } | |
| // MARK: - State enum | |
| private extension PlayerManager { | |
| enum State { | |
| case playing(AVPlayerItem?) | |
| case paused(AVPlayerItem?) | |
| case loading(AVPlayerItem?, Bool) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment