2022-03-07 20:46:59 +01:00
|
|
|
//
|
|
|
|
// AudioPlayer.swift
|
|
|
|
// App
|
|
|
|
//
|
|
|
|
// Created by Rasmus Krämer on 07.03.22.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import AVFoundation
|
|
|
|
import UIKit
|
|
|
|
import MediaPlayer
|
2023-06-04 12:21:40 -05:00
|
|
|
import RealmSwift
|
2022-03-07 20:46:59 +01:00
|
|
|
|
2023-02-09 02:38:56 +01:00
|
|
|
enum PlayMethod: Int {
|
2022-05-06 18:17:45 -05:00
|
|
|
case directplay = 0
|
|
|
|
case directstream = 1
|
|
|
|
case transcode = 2
|
|
|
|
case local = 3
|
|
|
|
}
|
|
|
|
|
2023-02-09 02:38:56 +01:00
|
|
|
enum PlayerStatus: Int {
|
|
|
|
case uninitialized = -1
|
|
|
|
case paused = 0
|
|
|
|
case playing = 1
|
|
|
|
}
|
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
class AudioPlayer: NSObject {
|
2022-09-02 16:31:47 -04:00
|
|
|
internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
|
2022-09-06 21:32:32 -04:00
|
|
|
internal let logger = AppLogger(category: "AudioPlayer")
|
2023-02-09 02:38:56 +01:00
|
|
|
|
|
|
|
private var status: PlayerStatus
|
2024-04-14 11:45:03 -07:00
|
|
|
internal var rateManager: AudioPlayerRateManager
|
2022-03-07 20:46:59 +01:00
|
|
|
|
|
|
|
private var playerContext = 0
|
|
|
|
private var playerItemContext = 0
|
|
|
|
|
2023-02-19 11:47:05 -06:00
|
|
|
internal var playWhenReady: Bool
|
2022-04-23 15:40:18 -05:00
|
|
|
private var initialPlaybackRate: Float
|
2022-03-07 20:46:59 +01:00
|
|
|
|
2022-09-02 16:31:47 -04:00
|
|
|
internal var audioPlayer: AVQueuePlayer
|
2022-08-16 12:32:22 -04:00
|
|
|
private var sessionId: String
|
2022-05-01 12:19:31 -05:00
|
|
|
|
2022-08-21 12:36:29 -04:00
|
|
|
private var timeObserverToken: Any?
|
2023-01-24 17:34:53 -05:00
|
|
|
private var sleepTimerObserverToken: Any?
|
2022-05-01 12:19:31 -05:00
|
|
|
private var queueObserver:NSKeyValueObservation?
|
|
|
|
private var queueItemStatusObserver:NSKeyValueObservation?
|
|
|
|
|
2022-09-02 16:31:47 -04:00
|
|
|
// Sleep timer values
|
|
|
|
internal var sleepTimeChapterStopAt: Double?
|
2022-09-02 18:22:42 -04:00
|
|
|
internal var sleepTimeChapterToken: Any?
|
|
|
|
internal var sleepTimer: Timer?
|
|
|
|
internal var sleepTimeRemaining: Double?
|
2022-08-22 17:04:48 -04:00
|
|
|
|
2022-09-02 18:31:16 -04:00
|
|
|
internal var currentTrackIndex = 0
|
2022-05-01 12:19:31 -05:00
|
|
|
private var allPlayerItems:[AVPlayerItem] = []
|
2022-03-07 20:46:59 +01:00
|
|
|
|
2022-03-28 22:09:43 +02:00
|
|
|
// MARK: - Constructor
|
2022-08-16 12:32:22 -04:00
|
|
|
init(sessionId: String, playWhenReady: Bool = false, playbackRate: Float = 1) {
|
2022-03-07 20:46:59 +01:00
|
|
|
self.playWhenReady = playWhenReady
|
2022-04-23 15:40:18 -05:00
|
|
|
self.initialPlaybackRate = playbackRate
|
2022-05-01 12:19:31 -05:00
|
|
|
self.audioPlayer = AVQueuePlayer()
|
2023-02-16 22:53:37 +01:00
|
|
|
self.audioPlayer.automaticallyWaitsToMinimizeStalling = true
|
2022-08-16 12:32:22 -04:00
|
|
|
self.sessionId = sessionId
|
2023-02-09 02:38:56 +01:00
|
|
|
self.status = .uninitialized
|
2024-04-14 11:45:03 -07:00
|
|
|
self.rateManager = LegacyAudioPlayerRateManager(audioPlayer: self.audioPlayer, defaultRate: playbackRate)
|
2022-03-07 20:46:59 +01:00
|
|
|
|
|
|
|
super.init()
|
|
|
|
|
2024-04-14 11:45:03 -07:00
|
|
|
self.rateManager.rateChangedCompletion = self.updateNowPlaying
|
|
|
|
self.rateManager.defaultRateChangedCompletion = self.setupTimeObservers
|
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
initAudioSession()
|
|
|
|
setupRemoteTransportControls()
|
|
|
|
|
2022-08-30 17:05:06 -04:00
|
|
|
let playbackSession = self.getPlaybackSession()
|
|
|
|
guard let playbackSession = playbackSession else {
|
2022-09-06 21:32:32 -04:00
|
|
|
logger.error("Failed to fetch playback session. Player will not initialize")
|
2022-08-30 17:05:06 -04:00
|
|
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
|
|
|
return
|
|
|
|
}
|
2022-08-16 12:32:22 -04:00
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
// Listen to player events
|
2022-08-30 22:59:59 -04:00
|
|
|
self.setupAudioSessionNotifications()
|
2022-03-07 20:46:59 +01:00
|
|
|
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext)
|
|
|
|
|
2022-05-01 12:19:31 -05:00
|
|
|
for track in playbackSession.audioTracks {
|
2023-06-04 12:21:40 -05:00
|
|
|
|
|
|
|
// TODO: All of this to get the ino of the file on the server. Future server release will include the ino with the session tracks
|
|
|
|
var audioFileIno = ""
|
|
|
|
let trackPath = track.metadata?.path ?? ""
|
|
|
|
if (!playbackSession.isLocal && playbackSession.episodeId != nil) {
|
|
|
|
let episodes = playbackSession.libraryItem?.media?.episodes ?? List<PodcastEpisode>()
|
|
|
|
let matchingEpisode:PodcastEpisode? = episodes.first(where: { $0.audioFile?.metadata?.path == trackPath })
|
|
|
|
audioFileIno = matchingEpisode?.audioFile?.ino ?? ""
|
|
|
|
} else if (!playbackSession.isLocal) {
|
|
|
|
let audioFiles = playbackSession.libraryItem?.media?.audioFiles ?? List<AudioFile>()
|
|
|
|
let matchingAudioFile = audioFiles.first(where: { $0.metadata?.path == trackPath })
|
|
|
|
audioFileIno = matchingAudioFile?.ino ?? ""
|
|
|
|
}
|
|
|
|
|
|
|
|
if let playerAsset = createAsset(itemId: playbackSession.libraryItemId!, track: track, ino: audioFileIno) {
|
2022-08-30 17:05:06 -04:00
|
|
|
let playerItem = AVPlayerItem(asset: playerAsset)
|
2023-03-27 16:54:19 -05:00
|
|
|
if (playbackSession.playMethod == PlayMethod.transcode.rawValue) {
|
|
|
|
playerItem.preferredForwardBufferDuration = 50
|
|
|
|
}
|
2022-08-30 17:05:06 -04:00
|
|
|
self.allPlayerItems.append(playerItem)
|
|
|
|
}
|
2022-05-01 12:19:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
self.currentTrackIndex = getItemIndexForTime(time: playbackSession.currentTime)
|
2022-09-08 20:09:35 -04:00
|
|
|
logger.log("Starting track index \(self.currentTrackIndex) for start time \(playbackSession.currentTime)")
|
2022-03-07 20:46:59 +01:00
|
|
|
|
2022-05-01 12:19:31 -05:00
|
|
|
let playerItems = self.allPlayerItems[self.currentTrackIndex..<self.allPlayerItems.count]
|
2022-09-08 20:09:35 -04:00
|
|
|
logger.log("Setting player items \(playerItems.count)")
|
2022-03-07 20:46:59 +01:00
|
|
|
|
2022-05-01 12:19:31 -05:00
|
|
|
for item in Array(playerItems) {
|
|
|
|
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
|
|
|
|
}
|
|
|
|
|
2023-01-24 17:34:53 -05:00
|
|
|
setupTimeObservers()
|
2022-05-01 12:19:31 -05:00
|
|
|
setupQueueObserver()
|
|
|
|
setupQueueItemStatusObserver()
|
|
|
|
|
2022-09-08 20:09:35 -04:00
|
|
|
logger.log("Audioplayer ready")
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
2022-09-03 16:34:31 -04:00
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
deinit {
|
2022-05-01 12:19:31 -05:00
|
|
|
self.queueObserver?.invalidate()
|
|
|
|
self.queueItemStatusObserver?.invalidate()
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
2022-09-03 16:34:31 -04:00
|
|
|
|
2022-03-28 22:09:43 +02:00
|
|
|
public func destroy() {
|
2022-04-28 18:05:33 -05:00
|
|
|
// Pause is not synchronous causing this error on below lines:
|
|
|
|
// AVAudioSession_iOS.mm:1206 Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session
|
2022-05-03 12:55:13 +02:00
|
|
|
// It is related to L79 `AVAudioSession.sharedInstance().setActive(false)`
|
2022-09-03 16:34:31 -04:00
|
|
|
self.pause()
|
|
|
|
self.audioPlayer.replaceCurrentItem(with: nil)
|
2022-03-07 20:46:59 +01:00
|
|
|
|
2022-03-22 15:38:31 +01:00
|
|
|
do {
|
|
|
|
try AVAudioSession.sharedInstance().setActive(false)
|
|
|
|
} catch {
|
2022-09-06 21:32:32 -04:00
|
|
|
logger.error("Failed to set AVAudioSession inactive")
|
|
|
|
logger.error(error)
|
2022-03-22 15:38:31 +01:00
|
|
|
}
|
|
|
|
|
2022-08-30 22:59:59 -04:00
|
|
|
self.removeAudioSessionNotifications()
|
2022-05-03 12:55:13 +02:00
|
|
|
DispatchQueue.runOnMainQueue {
|
|
|
|
UIApplication.shared.endReceivingRemoteControlEvents()
|
|
|
|
}
|
2022-09-03 16:34:31 -04:00
|
|
|
|
|
|
|
// Remove observers
|
|
|
|
self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), context: &playerContext)
|
2023-01-24 17:34:53 -05:00
|
|
|
self.removeTimeObservers()
|
2022-09-03 16:34:31 -04:00
|
|
|
|
2022-04-14 14:39:09 +02:00
|
|
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
|
2022-09-15 20:49:25 -04:00
|
|
|
|
|
|
|
// Remove timers
|
|
|
|
self.removeSleepTimer()
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
|
|
|
|
2022-08-30 17:05:06 -04:00
|
|
|
public func isInitialized() -> Bool {
|
2023-02-09 02:38:56 +01:00
|
|
|
return self.status != .uninitialized
|
2022-08-16 21:14:33 -04:00
|
|
|
}
|
|
|
|
|
2022-08-30 17:05:06 -04:00
|
|
|
public func getPlaybackSession() -> PlaybackSession? {
|
|
|
|
return Database.shared.getPlaybackSession(id: self.sessionId)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func getItemIndexForTime(time:Double) -> Int {
|
|
|
|
guard let playbackSession = self.getPlaybackSession() else { return 0 }
|
2022-05-01 12:19:31 -05:00
|
|
|
for index in 0..<self.allPlayerItems.count {
|
|
|
|
let startOffset = playbackSession.audioTracks[index].startOffset ?? 0.0
|
|
|
|
let duration = playbackSession.audioTracks[index].duration
|
|
|
|
let trackEnd = startOffset + duration
|
|
|
|
if (time < trackEnd.rounded(.down)) {
|
|
|
|
return index
|
2023-02-19 17:42:45 +01:00
|
|
|
} else if (index == self.allPlayerItems.count - 1) {
|
|
|
|
// Seeking past end of last item
|
|
|
|
return index
|
2022-05-01 12:19:31 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
2022-08-30 22:59:59 -04:00
|
|
|
private func setupAudioSessionNotifications() {
|
2022-08-30 22:47:55 -04:00
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(handleInteruption), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance())
|
2022-08-30 22:59:59 -04:00
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(handleRouteChange), name: AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance())
|
2022-08-30 22:47:55 -04:00
|
|
|
}
|
|
|
|
|
2022-08-30 22:59:59 -04:00
|
|
|
private func removeAudioSessionNotifications() {
|
2022-08-30 22:47:55 -04:00
|
|
|
NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance())
|
2022-08-30 22:59:59 -04:00
|
|
|
NotificationCenter.default.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance())
|
2022-08-30 22:47:55 -04:00
|
|
|
}
|
|
|
|
|
2023-01-24 17:34:53 -05:00
|
|
|
private func setupTimeObservers() {
|
2022-08-25 18:31:09 -04:00
|
|
|
// Time observer should be configured on the main queue
|
2022-08-26 19:11:55 -04:00
|
|
|
DispatchQueue.runOnMainQueue {
|
2023-01-24 17:34:53 -05:00
|
|
|
self.removeTimeObservers()
|
2022-08-25 18:28:17 -04:00
|
|
|
|
2022-08-25 18:31:09 -04:00
|
|
|
let timeScale = CMTimeScale(NSEC_PER_SEC)
|
2023-01-24 17:34:53 -05:00
|
|
|
// Save the current time every 15 seconds
|
|
|
|
var seconds = 15.0
|
|
|
|
var time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale)
|
2022-08-26 19:11:55 -04:00
|
|
|
self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: self.queue) { [weak self] time in
|
2022-08-29 21:16:51 -04:00
|
|
|
guard let self = self else { return }
|
2022-09-18 12:43:52 -04:00
|
|
|
guard self.isInitialized() else { return }
|
2022-08-29 21:16:51 -04:00
|
|
|
|
2022-08-30 17:05:06 -04:00
|
|
|
guard let currentTime = self.getCurrentTime() else { return }
|
2022-08-29 21:16:51 -04:00
|
|
|
let isPlaying = self.isPlaying()
|
2022-08-29 20:39:55 -04:00
|
|
|
|
2022-08-25 18:31:09 -04:00
|
|
|
Task {
|
|
|
|
// Let the player update the current playback positions
|
2022-08-29 21:16:51 -04:00
|
|
|
await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: isPlaying, isStopping: false)
|
2022-08-25 18:31:09 -04:00
|
|
|
}
|
2023-01-24 17:34:53 -05:00
|
|
|
}
|
|
|
|
// Update the sleep timer every second
|
|
|
|
seconds = 1.0
|
|
|
|
time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale)
|
|
|
|
self.sleepTimerObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: self.queue) { [weak self] time in
|
|
|
|
guard let self = self else { return }
|
2022-09-02 18:22:42 -04:00
|
|
|
if self.isSleepTimerSet() {
|
|
|
|
// Update the UI
|
2022-08-25 18:31:09 -04:00
|
|
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
|
|
|
|
}
|
2023-12-22 21:01:37 -05:00
|
|
|
// Update the now playing and chapter info
|
|
|
|
self.updateNowPlaying()
|
2022-08-22 17:04:48 -04:00
|
|
|
}
|
2022-08-21 12:36:29 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-24 17:34:53 -05:00
|
|
|
private func removeTimeObservers() {
|
2022-08-21 12:36:29 -04:00
|
|
|
if let timeObserverToken = timeObserverToken {
|
|
|
|
self.audioPlayer.removeTimeObserver(timeObserverToken)
|
|
|
|
self.timeObserverToken = nil
|
|
|
|
}
|
2023-01-24 17:34:53 -05:00
|
|
|
if let sleepTimerObserverToken = sleepTimerObserverToken {
|
|
|
|
self.audioPlayer.removeTimeObserver(sleepTimerObserverToken)
|
|
|
|
self.sleepTimerObserverToken = nil
|
|
|
|
}
|
2022-08-21 12:36:29 -04:00
|
|
|
}
|
|
|
|
|
2022-08-30 17:05:06 -04:00
|
|
|
private func setupQueueObserver() {
|
2022-09-03 16:34:31 -04:00
|
|
|
self.queueObserver = self.audioPlayer.observe(\.currentItem, options: [.new]) { [weak self] _,_ in
|
|
|
|
guard let self = self else { return }
|
2022-05-01 12:19:31 -05:00
|
|
|
let prevTrackIndex = self.currentTrackIndex
|
|
|
|
self.audioPlayer.currentItem.map { item in
|
|
|
|
self.currentTrackIndex = self.allPlayerItems.firstIndex(of:item) ?? 0
|
|
|
|
if (self.currentTrackIndex != prevTrackIndex) {
|
2022-09-08 20:09:35 -04:00
|
|
|
self.logger.log("New Current track index \(self.currentTrackIndex)")
|
2022-05-01 12:19:31 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-30 17:05:06 -04:00
|
|
|
private func setupQueueItemStatusObserver() {
|
2022-09-08 20:09:35 -04:00
|
|
|
logger.log("queueStatusObserver: Setting up")
|
2022-09-01 17:40:49 -04:00
|
|
|
|
|
|
|
// Listen for player item updates
|
2022-05-01 12:19:31 -05:00
|
|
|
self.queueItemStatusObserver?.invalidate()
|
2022-09-03 16:34:31 -04:00
|
|
|
self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { [weak self] playerItem, change in
|
|
|
|
self?.handleQueueItemStatus(playerItem: playerItem)
|
2022-08-31 23:15:25 -04:00
|
|
|
})
|
2022-09-01 17:40:49 -04:00
|
|
|
|
|
|
|
// Ensure we didn't miss a player item update during initialization
|
|
|
|
if let playerItem = self.audioPlayer.currentItem {
|
|
|
|
self.handleQueueItemStatus(playerItem: playerItem)
|
|
|
|
}
|
2022-08-31 23:15:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
private func handleQueueItemStatus(playerItem: AVPlayerItem) {
|
2022-09-08 20:09:35 -04:00
|
|
|
logger.log("queueStatusObserver: Current item status changed")
|
2022-08-31 23:15:25 -04:00
|
|
|
guard let playbackSession = self.getPlaybackSession() else {
|
|
|
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (playerItem.status == .readyToPlay) {
|
2022-09-08 20:09:35 -04:00
|
|
|
logger.log("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
|
2022-08-31 23:15:25 -04:00
|
|
|
|
|
|
|
// Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session
|
2023-02-09 02:38:56 +01:00
|
|
|
let firstReady = self.status == .uninitialized
|
2022-09-01 18:41:21 -04:00
|
|
|
if firstReady && !self.playWhenReady {
|
2022-09-01 18:19:45 -04:00
|
|
|
// Seek is async, and if we call this when also pressing play, we will get weird jumps in the scrub bar depending on timing
|
|
|
|
// Seeking to the correct position happens during play()
|
2022-08-31 23:15:25 -04:00
|
|
|
self.seek(playbackSession.currentTime, from: "queueItemStatusObserver")
|
2022-08-30 17:05:06 -04:00
|
|
|
}
|
2022-08-31 23:15:25 -04:00
|
|
|
|
|
|
|
// Start the player, if requested
|
|
|
|
if self.playWhenReady {
|
|
|
|
self.playWhenReady = false
|
2022-09-18 13:37:54 -04:00
|
|
|
self.play(allowSeekBack: false, isInitializing: true)
|
|
|
|
} else {
|
|
|
|
// Mark the player as ready
|
2023-02-09 02:38:56 +01:00
|
|
|
self.status = .paused
|
2022-05-01 12:19:31 -05:00
|
|
|
}
|
2022-08-31 23:15:25 -04:00
|
|
|
} else if (playerItem.status == .failed) {
|
2022-09-06 21:32:32 -04:00
|
|
|
logger.error("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
|
2022-08-31 23:15:25 -04:00
|
|
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
|
|
|
}
|
2022-05-01 12:19:31 -05:00
|
|
|
}
|
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
// MARK: - Methods
|
2022-09-18 13:37:54 -04:00
|
|
|
public func play(allowSeekBack: Bool = false, isInitializing: Bool = false) {
|
|
|
|
guard self.isInitialized() || isInitializing else { return }
|
2022-09-01 18:19:45 -04:00
|
|
|
guard let session = self.getPlaybackSession() else {
|
|
|
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
|
|
|
return
|
|
|
|
}
|
2022-08-21 11:05:16 -04:00
|
|
|
|
2022-09-01 18:19:45 -04:00
|
|
|
// Determine where we are starting playback
|
2022-09-20 18:34:43 -04:00
|
|
|
let currentTime = allowSeekBack ? PlayerTimeUtils.calcSeekBackTime(currentTime: session.currentTime, lastPlayedMs: session.updatedAt) : session.currentTime
|
2022-08-25 17:39:06 -04:00
|
|
|
|
2022-09-01 18:19:45 -04:00
|
|
|
// Sync our new playback position
|
|
|
|
Task { await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: self.isPlaying(), isStopping: false) }
|
|
|
|
|
|
|
|
// Start playback, with a seek, for as smooth a scrub bar start as possible
|
|
|
|
let currentTrackStartOffset = session.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
|
|
|
|
let seekTime = currentTime - currentTrackStartOffset
|
2022-09-13 20:47:07 -04:00
|
|
|
|
|
|
|
DispatchQueue.runOnMainQueue {
|
|
|
|
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] completed in
|
|
|
|
guard completed else { return }
|
|
|
|
self?.resumePlayback()
|
|
|
|
}
|
2022-08-22 17:04:48 -04:00
|
|
|
}
|
2022-09-01 18:19:45 -04:00
|
|
|
}
|
|
|
|
|
2022-09-02 18:22:42 -04:00
|
|
|
private func resumePlayback() {
|
2022-09-08 20:09:35 -04:00
|
|
|
logger.log("PLAY: Resuming playback")
|
2022-08-22 18:00:37 -04:00
|
|
|
|
2022-08-30 22:33:55 -04:00
|
|
|
self.markAudioSessionAs(active: true)
|
2022-09-13 20:47:07 -04:00
|
|
|
DispatchQueue.runOnMainQueue {
|
|
|
|
self.audioPlayer.play()
|
2024-04-14 11:45:03 -07:00
|
|
|
self.rateManager.handlePlayEvent()
|
2022-09-13 20:47:07 -04:00
|
|
|
}
|
2023-02-09 02:38:56 +01:00
|
|
|
self.status = .playing
|
|
|
|
|
2022-09-02 18:22:42 -04:00
|
|
|
// Update the progress
|
|
|
|
self.updateNowPlaying()
|
2022-09-18 14:21:41 -04:00
|
|
|
|
|
|
|
// Handle a chapter sleep timer that may now be invalid
|
|
|
|
self.handleTrackChangeForChapterSleepTimer()
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
2022-05-01 12:19:31 -05:00
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
public func pause() {
|
2022-08-21 11:05:16 -04:00
|
|
|
guard self.isInitialized() else { return }
|
|
|
|
|
2022-09-08 20:09:35 -04:00
|
|
|
logger.log("PAUSE: Pausing playback")
|
2022-09-13 20:47:07 -04:00
|
|
|
DispatchQueue.runOnMainQueue {
|
|
|
|
self.audioPlayer.pause()
|
|
|
|
}
|
2022-08-30 22:33:55 -04:00
|
|
|
self.markAudioSessionAs(active: false)
|
2022-03-07 20:46:59 +01:00
|
|
|
|
2022-08-22 17:04:48 -04:00
|
|
|
Task {
|
2022-08-30 17:05:06 -04:00
|
|
|
if let currentTime = self.getCurrentTime() {
|
|
|
|
await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: self.isPlaying(), isStopping: true)
|
|
|
|
}
|
2022-08-22 17:04:48 -04:00
|
|
|
}
|
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
|
2023-02-09 02:38:56 +01:00
|
|
|
self.status = .paused
|
2022-03-07 20:46:59 +01:00
|
|
|
updateNowPlaying()
|
|
|
|
}
|
2022-05-01 12:19:31 -05:00
|
|
|
|
2022-05-03 15:01:30 +02:00
|
|
|
public func seek(_ to: Double, from: String) {
|
2023-02-09 02:38:56 +01:00
|
|
|
logger.log("SEEK: Seek to \(to) from \(from)")
|
|
|
|
|
2022-08-30 17:05:06 -04:00
|
|
|
guard let playbackSession = self.getPlaybackSession() else { return }
|
2023-02-19 17:42:45 +01:00
|
|
|
|
2022-05-01 12:19:31 -05:00
|
|
|
let indexOfSeek = getItemIndexForTime(time: to)
|
2022-09-08 20:09:35 -04:00
|
|
|
logger.log("SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
|
2022-05-01 12:19:31 -05:00
|
|
|
|
|
|
|
// Reconstruct queue if seeking to a different track
|
|
|
|
if (self.currentTrackIndex != indexOfSeek) {
|
2023-11-29 17:30:34 +01:00
|
|
|
// When we seek to a different track, we need to make sure to seek the old track to 0
|
|
|
|
// or we will get jumps to the old position when fading over into a new track
|
|
|
|
self.audioPlayer.seek(to: CMTime(seconds: 0, preferredTimescale: 1000))
|
|
|
|
|
2022-05-01 12:19:31 -05:00
|
|
|
self.currentTrackIndex = indexOfSeek
|
|
|
|
|
2022-08-25 15:42:37 -04:00
|
|
|
try? playbackSession.update {
|
2022-08-16 12:32:22 -04:00
|
|
|
playbackSession.currentTime = to
|
|
|
|
}
|
2022-05-01 12:19:31 -05:00
|
|
|
|
|
|
|
let playerItems = self.allPlayerItems[indexOfSeek..<self.allPlayerItems.count]
|
|
|
|
|
2022-09-13 20:47:07 -04:00
|
|
|
DispatchQueue.runOnMainQueue {
|
|
|
|
self.audioPlayer.removeAllItems()
|
|
|
|
for item in Array(playerItems) {
|
|
|
|
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
|
|
|
|
}
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
2023-02-19 17:42:45 +01:00
|
|
|
|
|
|
|
seekInCurrentTrack(to: to, playbackSession: playbackSession)
|
|
|
|
|
2022-05-01 12:19:31 -05:00
|
|
|
setupQueueItemStatusObserver()
|
|
|
|
} else {
|
2023-02-19 17:42:45 +01:00
|
|
|
seekInCurrentTrack(to: to, playbackSession: playbackSession)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only for use in here where we handle track selection
|
|
|
|
func seekInCurrentTrack(to: Double, playbackSession: PlaybackSession) {
|
|
|
|
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
|
|
|
|
let ctso = currentTrack.startOffset ?? 0.0
|
|
|
|
let trackEnd = ctso + currentTrack.duration
|
|
|
|
logger.log("SEEK: Seeking in current item \(to) (track START = \(ctso) END = \(trackEnd))")
|
|
|
|
|
|
|
|
let boundedTime = min(max(to, ctso), trackEnd)
|
|
|
|
let seekTime = boundedTime - ctso
|
|
|
|
|
2022-09-13 20:47:07 -04:00
|
|
|
DispatchQueue.runOnMainQueue {
|
|
|
|
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
|
2023-02-09 02:38:56 +01:00
|
|
|
self?.logger.log("SEEK: Completion handler called")
|
2022-09-14 19:55:42 -04:00
|
|
|
guard completed else {
|
|
|
|
self?.logger.log("SEEK: WARNING: seeking not completed (to \(seekTime)")
|
|
|
|
return
|
|
|
|
}
|
2022-09-13 20:47:07 -04:00
|
|
|
guard let self = self else { return }
|
|
|
|
|
|
|
|
self.updateNowPlaying()
|
2022-08-22 17:04:48 -04:00
|
|
|
}
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-03-09 22:10:15 +01:00
|
|
|
|
2024-04-14 11:45:03 -07:00
|
|
|
public func setPlaybackRate(_ rate: Float) {
|
|
|
|
self.rateManager.setPlaybackRate(rate)
|
2022-08-22 17:04:48 -04:00
|
|
|
}
|
|
|
|
|
2023-12-22 21:01:37 -05:00
|
|
|
public func setChapterTrack() {
|
|
|
|
self.updateNowPlaying()
|
|
|
|
}
|
|
|
|
|
2022-08-30 17:05:06 -04:00
|
|
|
public func getCurrentTime() -> Double? {
|
|
|
|
guard let playbackSession = self.getPlaybackSession() else { return nil }
|
2022-05-01 12:19:31 -05:00
|
|
|
let currentTrackTime = self.audioPlayer.currentTime().seconds
|
|
|
|
let audioTrack = playbackSession.audioTracks[currentTrackIndex]
|
|
|
|
let startOffset = audioTrack.startOffset ?? 0.0
|
|
|
|
return startOffset + currentTrackTime
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
2022-08-13 12:41:20 -04:00
|
|
|
|
2022-08-30 17:05:06 -04:00
|
|
|
public func getPlayMethod() -> Int? {
|
|
|
|
guard let playbackSession = self.getPlaybackSession() else { return nil }
|
2022-08-16 12:32:22 -04:00
|
|
|
return playbackSession.playMethod
|
2022-05-06 18:17:45 -05:00
|
|
|
}
|
2022-08-29 20:39:55 -04:00
|
|
|
|
2022-08-16 12:32:22 -04:00
|
|
|
public func getPlaybackSessionId() -> String {
|
|
|
|
return self.sessionId
|
2022-05-06 18:17:45 -05:00
|
|
|
}
|
2022-08-29 20:39:55 -04:00
|
|
|
|
2022-08-30 17:05:06 -04:00
|
|
|
public func getDuration() -> Double? {
|
|
|
|
guard let playbackSession = self.getPlaybackSession() else { return nil }
|
2022-05-01 12:19:31 -05:00
|
|
|
return playbackSession.duration
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
|
|
|
|
2022-08-29 20:39:55 -04:00
|
|
|
public func isPlaying() -> Bool {
|
2023-02-09 02:38:56 +01:00
|
|
|
return self.status == .playing
|
2022-08-29 20:39:55 -04:00
|
|
|
}
|
|
|
|
|
2023-02-09 02:38:56 +01:00
|
|
|
public func getPlayerState() -> PlayerState {
|
|
|
|
switch status {
|
|
|
|
case .uninitialized:
|
|
|
|
return PlayerState.buffering
|
2023-02-16 23:00:33 +01:00
|
|
|
case .paused, .playing:
|
2023-02-09 02:38:56 +01:00
|
|
|
return PlayerState.ready
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
// MARK: - Private
|
2023-06-04 12:21:40 -05:00
|
|
|
private func createAsset(itemId:String, track:AudioTrack, ino:String) -> AVAsset? {
|
2022-08-30 17:05:06 -04:00
|
|
|
guard let playbackSession = self.getPlaybackSession() else { return nil }
|
|
|
|
|
2022-05-06 18:17:45 -05:00
|
|
|
if (playbackSession.playMethod == PlayMethod.directplay.rawValue) {
|
2023-06-04 12:21:40 -05:00
|
|
|
let urlstr = "\(Store.serverConfig!.address)/api/items/\(itemId)/file/\(ino)?token=\(Store.serverConfig!.token)"
|
2022-05-06 18:17:45 -05:00
|
|
|
let url = URL(string: urlstr)!
|
|
|
|
return AVURLAsset(url: url)
|
2022-08-11 18:29:55 -04:00
|
|
|
} else if (playbackSession.playMethod == PlayMethod.local.rawValue) {
|
|
|
|
guard let localFile = track.getLocalFile() else {
|
|
|
|
// Worst case we can stream the file
|
2022-09-08 20:09:35 -04:00
|
|
|
logger.log("Unable to play local file. Resulting to streaming \(track.localFileId ?? "Unknown")")
|
2023-06-04 12:21:40 -05:00
|
|
|
let urlstr = "\(Store.serverConfig!.address)/api/items/\(itemId)/file/\(ino)?token=\(Store.serverConfig!.token)"
|
2022-08-11 18:29:55 -04:00
|
|
|
let url = URL(string: urlstr)!
|
|
|
|
return AVURLAsset(url: url)
|
|
|
|
}
|
|
|
|
return AVURLAsset(url: localFile.contentPath)
|
2022-05-06 18:17:45 -05:00
|
|
|
} 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])
|
|
|
|
}
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
2022-05-01 12:19:31 -05:00
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
private func initAudioSession() {
|
|
|
|
do {
|
2022-08-26 18:58:08 -04:00
|
|
|
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio)
|
2022-03-07 20:46:59 +01:00
|
|
|
} catch {
|
2022-09-06 21:32:32 -04:00
|
|
|
logger.error("Failed to set AVAudioSession category")
|
|
|
|
logger.error(error)
|
2022-08-30 22:33:55 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func markAudioSessionAs(active: Bool) {
|
|
|
|
do {
|
|
|
|
try AVAudioSession.sharedInstance().setActive(active)
|
|
|
|
} catch {
|
2022-09-06 21:32:32 -04:00
|
|
|
logger.error("Failed to set audio session as active=\(active)")
|
2022-08-30 22:47:55 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-30 22:59:59 -04:00
|
|
|
// MARK: - iOS audio session notifications
|
2022-08-30 22:47:55 -04:00
|
|
|
@objc private func handleInteruption(notification: Notification) {
|
|
|
|
guard let userInfo = notification.userInfo,
|
|
|
|
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
|
|
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-07 18:02:25 -06:00
|
|
|
// When interruption is from the app suspending then don't resume playback
|
|
|
|
if #available(iOS 14.5, *) {
|
|
|
|
let reasonValue = userInfo[AVAudioSessionInterruptionReasonKey] as? UInt ?? 0
|
|
|
|
let reason = AVAudioSession.InterruptionReason(rawValue: reasonValue)
|
|
|
|
if (reason == .appWasSuspended) {
|
|
|
|
logger.log("AVAudioSession was suspended")
|
|
|
|
return
|
2022-08-30 22:47:55 -04:00
|
|
|
}
|
2024-02-07 18:02:25 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
switch type {
|
|
|
|
case .ended:
|
|
|
|
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
|
|
|
|
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
|
|
|
if options.contains(.shouldResume) {
|
|
|
|
self.play(allowSeekBack: true)
|
|
|
|
}
|
|
|
|
default: ()
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-30 22:59:59 -04:00
|
|
|
@objc private func handleRouteChange(notification: Notification) {
|
|
|
|
guard let userInfo = notification.userInfo,
|
|
|
|
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
|
|
|
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch reason {
|
|
|
|
case .newDeviceAvailable: // New device found.
|
|
|
|
let session = AVAudioSession.sharedInstance()
|
|
|
|
let headphonesConnected = hasHeadphones(in: session.currentRoute)
|
|
|
|
if headphonesConnected {
|
|
|
|
// We should just let things be, as it's okay to go from speaker to headphones
|
|
|
|
}
|
|
|
|
case .oldDeviceUnavailable: // Old device removed.
|
|
|
|
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
|
|
|
|
let headphonesWereConnected = hasHeadphones(in: previousRoute)
|
|
|
|
if headphonesWereConnected {
|
|
|
|
// Removing headphones we should pause instead of keeping on playing
|
|
|
|
self.pause()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
default: ()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func hasHeadphones(in routeDescription: AVAudioSessionRouteDescription) -> Bool {
|
|
|
|
// Filter the outputs to only those with a port type of headphones.
|
|
|
|
return !routeDescription.outputs.filter({$0.portType == .headphones}).isEmpty
|
|
|
|
}
|
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
// MARK: - Now playing
|
2024-02-25 15:04:54 -06:00
|
|
|
func setupRemoteTransportControls() {
|
2022-05-03 12:55:13 +02:00
|
|
|
DispatchQueue.runOnMainQueue {
|
2022-03-22 15:38:31 +01:00
|
|
|
UIApplication.shared.beginReceivingRemoteControlEvents()
|
2022-05-03 12:55:13 +02:00
|
|
|
}
|
2022-03-07 20:46:59 +01:00
|
|
|
let commandCenter = MPRemoteCommandCenter.shared()
|
2022-08-26 19:34:34 -04:00
|
|
|
let deviceSettings = Database.shared.getDeviceSettings()
|
2023-01-30 08:14:32 -06:00
|
|
|
let jumpForwardTime = deviceSettings.jumpForwardTime
|
|
|
|
let jumpBackwardsTime = deviceSettings.jumpBackwardsTime
|
2022-03-07 20:46:59 +01:00
|
|
|
|
|
|
|
commandCenter.playCommand.isEnabled = true
|
2022-09-03 16:34:31 -04:00
|
|
|
commandCenter.playCommand.addTarget { [weak self] event in
|
2023-12-30 11:16:10 +08:00
|
|
|
guard let strongSelf = self else { return .commandFailed }
|
|
|
|
if strongSelf.isPlaying() {
|
|
|
|
strongSelf.pause()
|
2023-12-23 01:51:37 -05:00
|
|
|
} else {
|
2023-12-30 11:16:10 +08:00
|
|
|
strongSelf.play(allowSeekBack: true)
|
2023-12-23 01:51:37 -05:00
|
|
|
}
|
2022-03-07 20:46:59 +01:00
|
|
|
return .success
|
|
|
|
}
|
2023-12-23 01:51:37 -05:00
|
|
|
|
2022-03-07 20:46:59 +01:00
|
|
|
commandCenter.pauseCommand.isEnabled = true
|
2022-09-03 16:34:31 -04:00
|
|
|
commandCenter.pauseCommand.addTarget { [weak self] event in
|
2023-12-30 11:16:10 +08:00
|
|
|
guard let strongSelf = self else { return .commandFailed }
|
|
|
|
if strongSelf.isPlaying() {
|
|
|
|
strongSelf.pause()
|
2023-12-23 01:51:37 -05:00
|
|
|
} else {
|
2023-12-30 11:16:10 +08:00
|
|
|
strongSelf.play(allowSeekBack: true)
|
2023-12-23 01:51:37 -05:00
|
|
|
}
|
|
|
|
return .success
|
|
|
|
}
|
|
|
|
|
|
|
|
commandCenter.togglePlayPauseCommand.isEnabled = true
|
|
|
|
commandCenter.togglePlayPauseCommand.addTarget { [weak self] event in
|
2023-12-30 11:16:10 +08:00
|
|
|
guard let strongSelf = self else { return .commandFailed }
|
|
|
|
if strongSelf.isPlaying() {
|
|
|
|
strongSelf.pause()
|
2023-12-23 01:51:37 -05:00
|
|
|
} else {
|
2023-12-30 11:16:10 +08:00
|
|
|
strongSelf.play(allowSeekBack: true)
|
2023-12-23 01:51:37 -05:00
|
|
|
}
|
2022-03-07 20:46:59 +01:00
|
|
|
return .success
|
|
|
|
}
|
|
|
|
|
|
|
|
commandCenter.skipForwardCommand.isEnabled = true
|
2023-01-30 08:14:32 -06:00
|
|
|
commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: jumpForwardTime)]
|
2022-09-03 16:34:31 -04:00
|
|
|
commandCenter.skipForwardCommand.addTarget { [weak self] event in
|
2022-03-07 20:46:59 +01:00
|
|
|
guard let command = event.command as? MPSkipIntervalCommand else {
|
|
|
|
return .noSuchContent
|
|
|
|
}
|
2022-09-03 16:34:31 -04:00
|
|
|
guard let currentTime = self?.getCurrentTime() else {
|
2022-08-30 17:05:06 -04:00
|
|
|
return .commandFailed
|
|
|
|
}
|
2022-09-03 16:34:31 -04:00
|
|
|
self?.seek(currentTime + command.preferredIntervals[0].doubleValue, from: "remote")
|
2022-03-07 20:46:59 +01:00
|
|
|
return .success
|
|
|
|
}
|
|
|
|
commandCenter.skipBackwardCommand.isEnabled = true
|
2023-01-30 08:14:32 -06:00
|
|
|
commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: jumpBackwardsTime)]
|
2022-09-03 16:34:31 -04:00
|
|
|
commandCenter.skipBackwardCommand.addTarget { [weak self] event in
|
2022-03-07 20:46:59 +01:00
|
|
|
guard let command = event.command as? MPSkipIntervalCommand else {
|
|
|
|
return .noSuchContent
|
|
|
|
}
|
2022-09-03 16:34:31 -04:00
|
|
|
guard let currentTime = self?.getCurrentTime() else {
|
2022-08-30 17:05:06 -04:00
|
|
|
return .commandFailed
|
|
|
|
}
|
2022-09-03 16:34:31 -04:00
|
|
|
self?.seek(currentTime - command.preferredIntervals[0].doubleValue, from: "remote")
|
2022-03-07 20:46:59 +01:00
|
|
|
return .success
|
|
|
|
}
|
|
|
|
|
2023-01-29 19:54:12 -08:00
|
|
|
commandCenter.nextTrackCommand.isEnabled = true
|
|
|
|
commandCenter.nextTrackCommand.addTarget { [weak self] _ in
|
|
|
|
guard let currentTime = self?.getCurrentTime() else {
|
|
|
|
return .commandFailed
|
|
|
|
}
|
2023-01-30 08:14:32 -06:00
|
|
|
self?.seek(currentTime + Double(jumpForwardTime), from: "remote")
|
2023-01-29 19:54:12 -08:00
|
|
|
return .success
|
|
|
|
}
|
|
|
|
commandCenter.previousTrackCommand.isEnabled = true
|
|
|
|
commandCenter.previousTrackCommand.addTarget { [weak self] _ in
|
|
|
|
guard let currentTime = self?.getCurrentTime() else {
|
|
|
|
return .commandFailed
|
|
|
|
}
|
2023-01-30 08:14:32 -06:00
|
|
|
self?.seek(currentTime - Double(jumpBackwardsTime), from: "remote")
|
2023-01-29 19:54:12 -08:00
|
|
|
return .success
|
|
|
|
}
|
|
|
|
|
2024-02-25 14:44:40 -06:00
|
|
|
commandCenter.changePlaybackPositionCommand.isEnabled = deviceSettings.allowSeekingOnMediaControls
|
2022-09-03 16:34:31 -04:00
|
|
|
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
|
2022-03-07 20:46:59 +01:00
|
|
|
guard let event = event as? MPChangePlaybackPositionCommandEvent else {
|
|
|
|
return .noSuchContent
|
|
|
|
}
|
|
|
|
|
2024-01-01 08:49:39 -06:00
|
|
|
// Adjust seek time if chapter track is being used
|
|
|
|
var seekTime = event.positionTime
|
|
|
|
if PlayerSettings.main().chapterTrack {
|
|
|
|
if let session = self?.getPlaybackSession(), let currentChapter = session.getCurrentChapter() {
|
|
|
|
seekTime += currentChapter.start
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self?.seek(seekTime, from: "remote")
|
2022-03-07 20:46:59 +01:00
|
|
|
return .success
|
|
|
|
}
|
2022-03-22 15:38:31 +01:00
|
|
|
|
|
|
|
commandCenter.changePlaybackRateCommand.isEnabled = true
|
2022-03-22 16:56:59 +01:00
|
|
|
commandCenter.changePlaybackRateCommand.supportedPlaybackRates = [0.5, 0.75, 1.0, 1.25, 1.5, 2]
|
2022-09-03 16:34:31 -04:00
|
|
|
commandCenter.changePlaybackRateCommand.addTarget { [weak self] event in
|
2022-03-22 15:38:31 +01:00
|
|
|
guard let event = event as? MPChangePlaybackRateCommandEvent else {
|
|
|
|
return .noSuchContent
|
|
|
|
}
|
|
|
|
|
2022-09-03 16:34:31 -04:00
|
|
|
self?.setPlaybackRate(event.playbackRate)
|
2022-03-22 15:38:31 +01:00
|
|
|
return .success
|
|
|
|
}
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
2022-03-28 22:09:43 +02:00
|
|
|
private func updateNowPlaying() {
|
2022-04-14 14:39:09 +02:00
|
|
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
2023-12-22 21:01:37 -05:00
|
|
|
if let session = self.getPlaybackSession(), let currentChapter = session.getCurrentChapter(), PlayerSettings.main().chapterTrack {
|
2024-01-01 08:49:39 -06:00
|
|
|
NowPlayingInfo.shared.update(
|
|
|
|
duration: currentChapter.getRelativeChapterEndTime(),
|
|
|
|
currentTime: currentChapter.getRelativeChapterCurrentTime(sessionCurrentTime: session.currentTime),
|
2024-04-14 11:45:03 -07:00
|
|
|
rate: self.rateManager.rate,
|
|
|
|
defaultRate: self.rateManager.defaultRate,
|
2024-01-01 08:49:39 -06:00
|
|
|
chapterName: currentChapter.title,
|
|
|
|
chapterNumber: (session.chapters.firstIndex(of: currentChapter) ?? 0) + 1,
|
|
|
|
chapterCount: session.chapters.count
|
|
|
|
)
|
|
|
|
} else if let duration = self.getDuration(), let currentTime = self.getCurrentTime() {
|
2024-04-14 11:45:03 -07:00
|
|
|
NowPlayingInfo.shared.update(duration: duration, currentTime: currentTime, rate: self.rateManager.rate, defaultRate: self.rateManager.defaultRate)
|
2022-08-30 17:05:06 -04:00
|
|
|
}
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Observer
|
|
|
|
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
2022-05-01 12:19:31 -05:00
|
|
|
if context == &playerContext {
|
2024-04-14 11:45:03 -07:00
|
|
|
if keyPath == #keyPath(AVPlayer.currentItem) {
|
2022-04-30 10:58:08 +02:00
|
|
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
2022-09-08 20:09:35 -04:00
|
|
|
logger.log("WARNING: Item ended")
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2022-03-28 22:09:43 +02:00
|
|
|
|
|
|
|
public static var instance: AudioPlayer?
|
2022-03-07 20:46:59 +01:00
|
|
|
}
|