// // AudioPlayer.swift // App // // Created by Rasmus Krämer on 07.03.22. // import Foundation import AVFoundation import UIKit import MediaPlayer enum PlayMethod:Int { case directplay = 0 case directstream = 1 case transcode = 2 case local = 3 } class AudioPlayer: NSObject { // enums and @objc are not compatible @objc dynamic var status: Int @objc dynamic var rate: Float private var tmpRate: Float = 1.0 private var lastPlayTime: Double = 0.0 private var playerContext = 0 private var playerItemContext = 0 private var playWhenReady: Bool private var initialPlaybackRate: Float private var audioPlayer: AVQueuePlayer private var playbackSession: PlaybackSession private var queueObserver:NSKeyValueObservation? private var queueItemStatusObserver:NSKeyValueObservation? private var currentTrackIndex = 0 private var allPlayerItems:[AVPlayerItem] = [] // MARK: - Constructor init(playbackSession: PlaybackSession, playWhenReady: Bool = false, playbackRate: Float = 1) { self.playWhenReady = playWhenReady self.initialPlaybackRate = playbackRate self.audioPlayer = AVQueuePlayer() self.playbackSession = playbackSession self.status = -1 self.rate = 0.0 self.tmpRate = playbackRate super.init() initAudioSession() setupRemoteTransportControls() // Listen to player events self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext) self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext) for track in playbackSession.audioTracks { let playerItem = AVPlayerItem(asset: createAsset(itemId: playbackSession.libraryItemId!, track: track)) self.allPlayerItems.append(playerItem) } self.currentTrackIndex = getItemIndexForTime(time: playbackSession.currentTime) NSLog("TEST: Starting track index \(self.currentTrackIndex) for start time \(playbackSession.currentTime)") let playerItems = self.allPlayerItems[self.currentTrackIndex.. Int { for index in 0.. 0.0 pause() NSLog("TEST: Seek to \(to) from \(from)") let currentTrack = self.playbackSession.audioTracks[self.currentTrackIndex] let ctso = currentTrack.startOffset ?? 0.0 let trackEnd = ctso + currentTrack.duration NSLog("TEST: Seek current track END = \(trackEnd)") let indexOfSeek = getItemIndexForTime(time: to) NSLog("TEST: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)") // Reconstruct queue if seeking to a different track if (self.currentTrackIndex != indexOfSeek) { self.currentTrackIndex = indexOfSeek self.playbackSession.currentTime = to self.playWhenReady = continuePlaying // Only playWhenReady if already playing self.status = -1 let playerItems = self.allPlayerItems[indexOfSeek.. 0.0 && !(observed && rate == 1) { self.tmpRate = rate } self.rate = rate self.updateNowPlaying() } public func getCurrentTime() -> Double { let currentTrackTime = self.audioPlayer.currentTime().seconds let audioTrack = playbackSession.audioTracks[currentTrackIndex] let startOffset = audioTrack.startOffset ?? 0.0 return startOffset + currentTrackTime } public func getPlayMethod() -> Int { return self.playbackSession.playMethod } public func getPlaybackSession() -> PlaybackSession { return self.playbackSession } public func getDuration() -> Double { return playbackSession.duration } // MARK: - Private private func createAsset(itemId:String, track:AudioTrack) -> AVAsset { if (playbackSession.playMethod == PlayMethod.directplay.rawValue) { // The only reason this is separate is because the filename needs to be encoded let filename = track.metadata?.filename ?? "" let filenameEncoded = filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed) let urlstr = "\(Store.serverConfig!.address)/s/item/\(itemId)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)" let url = URL(string: urlstr)! return AVURLAsset(url: url) } else { // HLS Transcode let headers: [String: String] = [ "Authorization": "Bearer \(Store.serverConfig!.token)" ] return AVURLAsset(url: URL(string: "\(Store.serverConfig!.address)\(track.contentUrl ?? "")")!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) } } private func initAudioSession() { do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [.allowAirPlay]) try AVAudioSession.sharedInstance().setActive(true) } catch { NSLog("Failed to set AVAudioSession category") print(error) } } // MARK: - Now playing private func setupRemoteTransportControls() { DispatchQueue.runOnMainQueue { UIApplication.shared.beginReceivingRemoteControlEvents() } let commandCenter = MPRemoteCommandCenter.shared() commandCenter.playCommand.isEnabled = true commandCenter.playCommand.addTarget { [unowned self] event in play(allowSeekBack: true) return .success } commandCenter.pauseCommand.isEnabled = true commandCenter.pauseCommand.addTarget { [unowned self] event in pause() return .success } commandCenter.skipForwardCommand.isEnabled = true commandCenter.skipForwardCommand.preferredIntervals = [30] commandCenter.skipForwardCommand.addTarget { [unowned self] event in guard let command = event.command as? MPSkipIntervalCommand else { return .noSuchContent } seek(getCurrentTime() + command.preferredIntervals[0].doubleValue, from: "remote") return .success } commandCenter.skipBackwardCommand.isEnabled = true commandCenter.skipBackwardCommand.preferredIntervals = [30] commandCenter.skipBackwardCommand.addTarget { [unowned self] event in guard let command = event.command as? MPSkipIntervalCommand else { return .noSuchContent } seek(getCurrentTime() - command.preferredIntervals[0].doubleValue, from: "remote") return .success } commandCenter.changePlaybackPositionCommand.isEnabled = true commandCenter.changePlaybackPositionCommand.addTarget { event in guard let event = event as? MPChangePlaybackPositionCommandEvent else { return .noSuchContent } self.seek(event.positionTime, from: "remote") return .success } commandCenter.changePlaybackRateCommand.isEnabled = true commandCenter.changePlaybackRateCommand.supportedPlaybackRates = [0.5, 0.75, 1.0, 1.25, 1.5, 2] commandCenter.changePlaybackRateCommand.addTarget { event in guard let event = event as? MPChangePlaybackRateCommandEvent else { return .noSuchContent } self.setPlaybackRate(event.playbackRate) return .success } } private func updateNowPlaying() { NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil) NowPlayingInfo.shared.update(duration: getDuration(), currentTime: getCurrentTime(), rate: rate) } // MARK: - Observer public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if context == &playerContext { if keyPath == #keyPath(AVPlayer.rate) { NSLog("TEST: playerContext observer player rate") self.setPlaybackRate(change?[.newKey] as? Float ?? 1.0, observed: true) } else if keyPath == #keyPath(AVPlayer.currentItem) { NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil) NSLog("WARNING: Item ended") } } else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) return } } public static var instance: AudioPlayer? }