From 27d2ed230446219b370c7ff523bf18992ee3730e Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Sun, 21 Aug 2022 12:06:37 -0400 Subject: [PATCH 01/66] Convert PlayerHandler to shared instance --- ios/App/App/plugins/AbsAudioPlayer.swift | 2 +- ios/App/Shared/player/PlayerHandler.swift | 6 +++--- ios/App/Shared/player/PlayerProgress.swift | 16 +++++++++------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/ios/App/App/plugins/AbsAudioPlayer.swift b/ios/App/App/plugins/AbsAudioPlayer.swift index 32c989f3..7d0dfb8c 100644 --- a/ios/App/App/plugins/AbsAudioPlayer.swift +++ b/ios/App/App/plugins/AbsAudioPlayer.swift @@ -40,7 +40,7 @@ public class AbsAudioPlayer: CAPPlugin { // Fetch the most recent active session let activeSession = try await Realm().objects(PlaybackSession.self).where({ $0.isActiveSession == true }).last if let activeSession = activeSession { - await PlayerProgress.syncFromServer() + await PlayerProgress.shared.syncFromServer() try self.startPlaybackSession(activeSession, playWhenReady: false, playbackRate: PlayerSettings.main().playbackRate) } } catch { diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index 6d840d54..3d572932 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -240,11 +240,11 @@ class PlayerHandler { listeningTimePassedSinceLastSync = 0 // Persist items in the database and sync to the server - if session.isLocal { PlayerProgress.syncFromPlayer() } - Task { await PlayerProgress.syncToServer() } + if session.isLocal { PlayerProgress.shared.syncFromPlayer() } + Task { await PlayerProgress.shared.syncToServer() } } @objc public static func syncServerProgressDuringPause() { - Task { await PlayerProgress.syncFromServer() } + Task { await PlayerProgress.shared.syncFromServer() } } } diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 0ee8961d..1576c24a 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -11,25 +11,27 @@ import RealmSwift class PlayerProgress { + public static let shared = PlayerProgress() + private init() {} - public static func syncFromPlayer() { + public func syncFromPlayer() { updateLocalMediaProgressFromLocalSession() } - public static func syncToServer() async { + public func syncToServer() async { let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncToServer") updateAllServerSessionFromLocalSession() await UIApplication.shared.endBackgroundTask(backgroundToken) } - public static func syncFromServer() async { + public func syncFromServer() async { let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromServer") await updateLocalSessionFromServerMediaProgress() await UIApplication.shared.endBackgroundTask(backgroundToken) } - private static func updateLocalMediaProgressFromLocalSession() { + private func updateLocalMediaProgressFromLocalSession() { guard let session = PlayerHandler.getPlaybackSession() else { return } guard session.isLocal else { return } @@ -49,7 +51,7 @@ class PlayerProgress { NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil) } - private static func updateAllServerSessionFromLocalSession() { + private func updateAllServerSessionFromLocalSession() { let sessions = try! Realm().objects(PlaybackSession.self).where({ $0.serverConnectionConfigId == Store.serverConfig?.id }) for session in sessions { let session = session.freeze() @@ -57,7 +59,7 @@ class PlayerProgress { } } - private static func updateServerSessionFromLocalSession(_ session: PlaybackSession) async { + private func updateServerSessionFromLocalSession(_ session: PlaybackSession) async { NSLog("Sending sessionId(\(session.id)) to server") var success = false @@ -75,7 +77,7 @@ class PlayerProgress { } } - private static func updateLocalSessionFromServerMediaProgress() async { + private func updateLocalSessionFromServerMediaProgress() async { NSLog("checkCurrentSessionProgress: Checking if local media progress was updated on server") guard let session = PlayerHandler.getPlaybackSession()?.freeze() else { return } From 8952cbfd20eec6cccdc2c76c1b274464bd2aff7b Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Sun, 21 Aug 2022 12:36:29 -0400 Subject: [PATCH 02/66] Start of refactor --- ios/App/Shared/player/AudioPlayer.swift | 18 ++++++++++++++++++ ios/App/Shared/player/PlayerHandler.swift | 11 ++++------- ios/App/Shared/player/PlayerProgress.swift | 8 +++++++- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 5c8db6a1..cb3bb1cf 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -34,6 +34,7 @@ class AudioPlayer: NSObject { private var audioPlayer: AVQueuePlayer private var sessionId: String + private var timeObserverToken: Any? private var queueObserver:NSKeyValueObservation? private var queueItemStatusObserver:NSKeyValueObservation? @@ -77,12 +78,14 @@ class AudioPlayer: NSObject { self.audioPlayer.insert(item, after:self.audioPlayer.items().last) } + setupTimeObserver() setupQueueObserver() setupQueueItemStatusObserver() NSLog("Audioplayer ready") } deinit { + self.removeTimeObserver() self.queueObserver?.invalidate() self.queueItemStatusObserver?.invalidate() destroy() @@ -124,6 +127,21 @@ class AudioPlayer: NSObject { return 0 } + private func setupTimeObserver() { + let timeScale = CMTimeScale(NSEC_PER_SEC) + let time = CMTime(seconds: 1, preferredTimescale: timeScale) + self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] currentTime in + NSLog("currentTime: \(currentTime)") + } + } + + private func removeTimeObserver() { + if let timeObserverToken = timeObserverToken { + self.audioPlayer.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } + } + func setupQueueObserver() { self.queueObserver = self.audioPlayer.observe(\.currentItem, options: [.new]) {_,_ in let prevTrackIndex = self.currentTrackIndex diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index 3d572932..c3b8efd2 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -141,23 +141,20 @@ class PlayerHandler { public static func getPlaybackSession() -> PlaybackSession? { guard let player = player else { return nil } + guard player.isInitialized() else { return nil } guard let session = Database.shared.getPlaybackSession(id: player.getPlaybackSessionId()) else { return nil } return session } public static func seekForward(amount: Double) { - guard let player = player else { - return - } + guard let player = player else { return } let destinationTime = player.getCurrentTime() + amount player.seek(destinationTime, from: "handler") } public static func seekBackward(amount: Double) { - guard let player = player else { - return - } + guard let player = player else { return } let destinationTime = player.getCurrentTime() - amount player.seek(destinationTime, from: "handler") @@ -240,7 +237,7 @@ class PlayerHandler { listeningTimePassedSinceLastSync = 0 // Persist items in the database and sync to the server - if session.isLocal { PlayerProgress.shared.syncFromPlayer() } + if session.isLocal { Task { await PlayerProgress.shared.syncFromPlayer() } } Task { await PlayerProgress.shared.syncToServer() } } diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 1576c24a..47251dee 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -15,8 +15,10 @@ class PlayerProgress { private init() {} - public func syncFromPlayer() { + public func syncFromPlayer() async { + let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromPlayer") updateLocalMediaProgressFromLocalSession() + await UIApplication.shared.endBackgroundTask(backgroundToken) } public func syncToServer() async { @@ -31,6 +33,10 @@ class PlayerProgress { await UIApplication.shared.endBackgroundTask(backgroundToken) } + private func updateLocalSessionFromPlayer() async { + + } + private func updateLocalMediaProgressFromLocalSession() { guard let session = PlayerHandler.getPlaybackSession() else { return } guard session.isLocal else { return } From a32bd9d74c7545d9341d55ce54c7aa15e3412d2c Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 22 Aug 2022 16:02:35 -0500 Subject: [PATCH 03/66] Update:Audio player menu to modal instead of dropdown --- components/app/AudioPlayer.vue | 18 ++++++++++++++---- components/modals/Dialog.vue | 12 +++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index 61a94a19..20ae55ae 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -8,9 +8,7 @@ cast
- - more_vert - + more_vert

{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}

@@ -95,6 +93,16 @@ + + + @@ -142,7 +150,8 @@ export default { isLoading: false, touchTrackStart: false, dragPercent: 0, - syncStatus: 0 + syncStatus: 0, + showMoreMenuDialog: false } }, watch: { @@ -612,6 +621,7 @@ export default { } }, clickMenuAction(action) { + this.showMoreMenuDialog = false if (action === 'chapter_track') { this.useChapterTrack = !this.useChapterTrack diff --git a/components/modals/Dialog.vue b/components/modals/Dialog.vue index f1c37bf4..31041ff8 100644 --- a/components/modals/Dialog.vue +++ b/components/modals/Dialog.vue @@ -10,11 +10,13 @@
From d57fe44bcc8b73ee50cb9d855c9164c80213c503 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 22 Aug 2022 17:04:48 -0400 Subject: [PATCH 04/66] Sleep timer using native time observer --- ios/App/App/plugins/AbsAudioPlayer.swift | 36 ++-- ios/App/Shared/player/AudioPlayer.swift | 137 ++++++++++++- ios/App/Shared/player/PlayerHandler.swift | 218 +++++++-------------- ios/App/Shared/player/PlayerProgress.swift | 45 ++++- 4 files changed, 264 insertions(+), 172 deletions(-) diff --git a/ios/App/App/plugins/AbsAudioPlayer.swift b/ios/App/App/plugins/AbsAudioPlayer.swift index 7d0dfb8c..6a2a40d5 100644 --- a/ios/App/App/plugins/AbsAudioPlayer.swift +++ b/ios/App/App/plugins/AbsAudioPlayer.swift @@ -166,45 +166,47 @@ public class AbsAudioPlayer: CAPPlugin { @objc func decreaseSleepTime(_ call: CAPPluginCall) { guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } - guard let time = Int(timeString) else { return call.resolve([ "success": false ]) } - guard let currentSleepTime = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) } + guard let time = Double(timeString) else { return call.resolve([ "success": false ]) } + guard let _ = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) } - PlayerHandler.remainingSleepTime = currentSleepTime - (time / 1000) + let seconds = time/1000 + PlayerHandler.decreaseSleepTime(decreaseSeconds: seconds) call.resolve() } + @objc func increaseSleepTime(_ call: CAPPluginCall) { guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } - guard let time = Int(timeString) else { return call.resolve([ "success": false ]) } - guard let currentSleepTime = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) } + guard let time = Double(timeString) else { return call.resolve([ "success": false ]) } + guard let _ = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) } - PlayerHandler.remainingSleepTime = currentSleepTime + (time / 1000) + let seconds = time/1000 + PlayerHandler.increaseSleepTime(increaseSeconds: seconds) call.resolve() } + @objc func setSleepTimer(_ call: CAPPluginCall) { guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let time = Int(timeString) else { return call.resolve([ "success": false ]) } - let timeSeconds = time / 1000 + let isChapterTime = call.getBool("isChapterTime", false) - NSLog("chapter time: \(call.getBool("isChapterTime", false))") + let seconds = time / 1000 - if call.getBool("isChapterTime", false) { - let timeToPause = timeSeconds - Int(PlayerHandler.getCurrentTime() ?? 0) - if timeToPause < 0 { return call.resolve([ "success": false ]) } - - PlayerHandler.sleepTimerChapterStopTime = timeSeconds - PlayerHandler.remainingSleepTime = timeToPause + NSLog("chapter time: \(isChapterTime)") + if isChapterTime { + PlayerHandler.setChapterSleepTime(stopAt: Double(seconds)) return call.resolve([ "success": true ]) } - PlayerHandler.sleepTimerChapterStopTime = nil - PlayerHandler.remainingSleepTime = timeSeconds + PlayerHandler.setSleepTime(secondsUntilSleep: Double(seconds)) call.resolve([ "success": true ]) } + @objc func cancelSleepTimer(_ call: CAPPluginCall) { - PlayerHandler.remainingSleepTime = nil + PlayerHandler.cancelSleepTime() PlayerHandler.sleepTimerChapterStopTime = nil call.resolve() } + @objc func getSleepTimerTime(_ call: CAPPluginCall) { call.resolve([ "value": PlayerHandler.remainingSleepTime diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index cb3bb1cf..e0ae87b5 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -38,6 +38,9 @@ class AudioPlayer: NSObject { private var queueObserver:NSKeyValueObservation? private var queueItemStatusObserver:NSKeyValueObservation? + private var sleepTimeStopAt: Double? + private var sleepTimeToken: Any? + private var currentTrackIndex = 0 private var allPlayerItems:[AVPlayerItem] = [] @@ -85,6 +88,7 @@ class AudioPlayer: NSObject { NSLog("Audioplayer ready") } deinit { + self.removeSleepTimer() self.removeTimeObserver() self.queueObserver?.invalidate() self.queueItemStatusObserver?.invalidate() @@ -129,9 +133,18 @@ class AudioPlayer: NSObject { private func setupTimeObserver() { let timeScale = CMTimeScale(NSEC_PER_SEC) - let time = CMTime(seconds: 1, preferredTimescale: timeScale) - self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] currentTime in - NSLog("currentTime: \(currentTime)") + // Observe multiple times per seconds, as rate will be different depending on playback speed + let time = CMTime(seconds: 0.25, preferredTimescale: timeScale) + self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) { time in + Task { + // Let the player update the current playback positions + await PlayerProgress.shared.syncFromPlayer(currentTime: time.seconds, includesPlayProgress: true, isStopping: false) + + // Update the sleep time, if set + if self.sleepTimeStopAt != nil { + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) + } + } } } @@ -210,6 +223,11 @@ class AudioPlayer: NSObject { } } lastPlayTime = Date.timeIntervalSinceReferenceDate + + Task { + let isPlaying = self.status > 0 + await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: isPlaying, isStopping: false) + } self.audioPlayer.play() self.status = 1 @@ -224,6 +242,10 @@ class AudioPlayer: NSObject { self.status = 0 self.rate = 0.0 + Task { + await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: true, isStopping: true) + } + updateNowPlaying() lastPlayTime = Date.timeIntervalSinceReferenceDate } @@ -242,6 +264,8 @@ class AudioPlayer: NSObject { let trackEnd = ctso + currentTrack.duration NSLog("Seek current track END = \(trackEnd)") + // Capture remaining sleep time before changing the track position + let sleepSecondsRemaining = PlayerHandler.remainingSleepTime let indexOfSeek = getItemIndexForTime(time: to) NSLog("Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)") @@ -269,15 +293,21 @@ class AudioPlayer: NSObject { let currentTrackStartOffset = playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0 let seekTime = to - currentTrackStartOffset - self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { completed in + self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in if !completed { NSLog("WARNING: seeking not completed (to \(seekTime)") } if continuePlaying { - self.play() + self?.play() + } + self?.updateNowPlaying() + + // If we have an active sleep timer, reschedule based on seek, since seek is fuzzy + // Theis needs to occur after play() to capture the correct rate + if let currentTime = self?.getCurrentTime() { + self?.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining) } - self.updateNowPlaying() } } } @@ -291,8 +321,103 @@ class AudioPlayer: NSObject { self.tmpRate = rate } + // Capture remaining sleep time before changing the rate + let sleepSecondsRemaining = PlayerHandler.remainingSleepTime + self.rate = rate self.updateNowPlaying() + + // If we have an active sleep timer, reschedule based on rate + self.rescheduleSleepTimerAtTime(time: self.getCurrentTime(), secondsRemaining: sleepSecondsRemaining) + } + + public func getSleepStopAt() -> Double? { + return self.sleepTimeStopAt + } + + // Let iOS handle the sleep timer logic by letting us know when it's time to stop + public func setSleepTime(stopAt: Double, scaleBasedOnSpeed: Bool = false) { + NSLog("SLEEP TIMER: Scheduling for \(stopAt)") + + // Reset any previous sleep timer + self.removeSleepTimer() + + let currentTime = getCurrentTime() + + // Mark the time to stop playing + if scaleBasedOnSpeed { + // Consider paused as playing at 1x + let rate = Double(self.rate > 0 ? self.rate : 1) + + // Calculate the scaled time to stop at + let timeUntilSleep = (stopAt - currentTime) * rate + self.sleepTimeStopAt = currentTime + timeUntilSleep + + NSLog("SLEEP TIMER: Adjusted based on playback speed of \(rate) to \(self.sleepTimeStopAt!)") + } else { + self.sleepTimeStopAt = stopAt + } + + guard let sleepTimeStopAt = self.sleepTimeStopAt else { return } + let sleepTime = CMTime(seconds: sleepTimeStopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + + // Schedule the observation time + var times = [NSValue]() + times.append(NSValue(time: sleepTime)) + + sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: .main) { [weak self] in + NSLog("SLEEP TIMER: Pausing audio") + self?.pause() + self?.removeSleepTimer() + } + + // Update the UI + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) + } + + private func rescheduleSleepTimerAtTime(time: Double, secondsRemaining: Int?) { + // Not a chapter sleep timer + guard PlayerHandler.sleepTimerChapterStopTime == nil else { return } + + // Update the sleep timer + if let secondsRemaining = secondsRemaining { + let newSleepTimerPosition = time + Double(secondsRemaining) + self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) + } + } + + public func increaseSleepTime(extraTimeInSeconds: Double) { + if let sleepTime = PlayerHandler.remainingSleepTime { + let currentTime = getCurrentTime() + let newSleepTimerPosition = currentTime + Double(sleepTime) + extraTimeInSeconds + if newSleepTimerPosition > currentTime { + self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) + } + } + } + + public func decreaseSleepTime(removeTimeInSeconds: Double) { + if let sleepTime = PlayerHandler.remainingSleepTime { + let currentTime = getCurrentTime() + let newSleepTimerPosition = currentTime + Double(sleepTime) - removeTimeInSeconds + guard newSleepTimerPosition > currentTime else { return } + if newSleepTimerPosition > currentTime { + self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) + } + } + + } + + public func removeSleepTimer() { + PlayerHandler.sleepTimerChapterStopTime = nil + self.sleepTimeStopAt = nil + if let token = sleepTimeToken { + self.audioPlayer.removeTimeObserver(token) + sleepTimeToken = nil + } + + // Update the UI + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self) } public func getCurrentTime() -> Double { diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index c3b8efd2..610b8f73 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -10,85 +10,8 @@ import RealmSwift class PlayerHandler { private static var player: AudioPlayer? - private static var playingTimer: Timer? - private static var pausedTimer: Timer? - private static var lastSyncTime: Double = 0.0 public static var sleepTimerChapterStopTime: Int? = nil - private static var _remainingSleepTime: Int? = nil - public static var remainingSleepTime: Int? { - get { - return _remainingSleepTime - } - set(time) { - if time != nil && time! < 0 { - _remainingSleepTime = nil - } else { - _remainingSleepTime = time - } - - if _remainingSleepTime == nil { - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: _remainingSleepTime) - } else { - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: _remainingSleepTime) - } - } - } - private static var listeningTimePassedSinceLastSync: Double = 0.0 - - public static var paused: Bool { - get { - guard let player = player else { - return true - } - - return player.rate == 0.0 - } - set(paused) { - if paused { - self.player?.pause() - } else { - self.player?.play() - self.pausedTimer?.invalidate() - } - } - } - - public static func startTickTimer() { - DispatchQueue.runOnMainQueue { - NSLog("Starting the tick timer") - playingTimer?.invalidate() - pausedTimer?.invalidate() - playingTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in - self.tick() - } - } - } - - public static func stopTickTimer() { - NSLog("Stopping the tick timer") - playingTimer?.invalidate() - pausedTimer?.invalidate() - playingTimer = nil - } - - private static func startPausedTimer() { - guard self.paused else { return } - self.pausedTimer?.invalidate() - self.pausedTimer = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(syncServerProgressDuringPause), userInfo: nil, repeats: true) - } - - private static func cleanupOldSessions(currentSessionId: String?) { - let realm = try! Realm() - let oldSessions = realm.objects(PlaybackSession.self) .where({ $0.isActiveSession == true }) - try! realm.write { - for s in oldSessions { - if s.id != currentSessionId { - s.isActiveSession = false - } - } - } - } public static func startPlayback(sessionId: String, playWhenReady: Bool, playbackRate: Float) { guard let session = Database.shared.getPlaybackSession(id: sessionId) else { return } @@ -99,26 +22,21 @@ class PlayerHandler { player = nil } - // Cleanup old sessions + // Cleanup and sync old sessions cleanupOldSessions(currentSessionId: sessionId) + Task { await PlayerProgress.shared.syncToServer() } // Set now playing info NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, artworkUrl: session.coverPath, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil)) // Create the audio player player = AudioPlayer(sessionId: sessionId, playWhenReady: playWhenReady, playbackRate: playbackRate) - - startTickTimer() - startPausedTimer() } public static func stopPlayback() { // Pause playback first, so we can sync our current progress player?.pause() - // Stop updating progress before we destory the player, so we don't receive bad data - stopTickTimer() - player?.destroy() player = nil @@ -127,6 +45,41 @@ class PlayerHandler { NowPlayingInfo.shared.reset() } + public static var paused: Bool { + get { + guard let player = player else { return true } + return player.rate == 0.0 + } + set(paused) { + if paused { + self.player?.pause() + } else { + self.player?.play() + } + } + } + + public static var remainingSleepTime: Int? { + get { + guard let player = player else { return nil } + + // Consider paused as playing at 1x + let rate = Double(player.rate > 0 ? player.rate : 1) + + if let sleepTimerChapterStopTime = sleepTimerChapterStopTime { + let timeUntilChapterEnd = Double(sleepTimerChapterStopTime) - player.getCurrentTime() + let timeUntilChapterEndScaled = timeUntilChapterEnd / rate + return Int(timeUntilChapterEndScaled.rounded()) + } else if let stopAt = player.getSleepStopAt() { + let timeUntilSleep = stopAt - player.getCurrentTime() + let timeUntilSleepScaled = timeUntilSleep / rate + return Int(timeUntilSleepScaled.rounded()) + } else { + return nil + } + } + } + public static func getCurrentTime() -> Double? { self.player?.getCurrentTime() } @@ -135,6 +88,30 @@ class PlayerHandler { self.player?.setPlaybackRate(speed) } + public static func setSleepTime(secondsUntilSleep: Double) { + guard let player = player else { return } + let stopAt = secondsUntilSleep + player.getCurrentTime() + player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: true) + } + + public static func setChapterSleepTime(stopAt: Double) { + guard let player = player else { return } + self.sleepTimerChapterStopTime = Int(stopAt) + player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: false) + } + + public static func increaseSleepTime(increaseSeconds: Double) { + self.player?.increaseSleepTime(extraTimeInSeconds: increaseSeconds) + } + + public static func decreaseSleepTime(decreaseSeconds: Double) { + self.player?.decreaseSleepTime(removeTimeInSeconds: decreaseSeconds) + } + + public static func cancelSleepTime() { + self.player?.removeSleepTimer() + } + public static func getPlayMethod() -> Int? { self.player?.getPlayMethod() } @@ -142,8 +119,8 @@ class PlayerHandler { public static func getPlaybackSession() -> PlaybackSession? { guard let player = player else { return nil } guard player.isInitialized() else { return nil } - guard let session = Database.shared.getPlaybackSession(id: player.getPlaybackSessionId()) else { return nil } - return session + + return Database.shared.getPlaybackSession(id: player.getPlaybackSessionId()) } public static func seekForward(amount: Double) { @@ -168,10 +145,6 @@ class PlayerHandler { guard let player = player else { return nil } guard player.isInitialized() else { return nil } - DispatchQueue.main.async { - syncPlayerProgress() - } - return [ "duration": player.getDuration(), "currentTime": player.getCurrentTime(), @@ -180,65 +153,18 @@ class PlayerHandler { ] } - private static func tick() { - if !paused { - listeningTimePassedSinceLastSync += 1 - - if remainingSleepTime != nil { - if sleepTimerChapterStopTime != nil { - let timeUntilChapterEnd = Double(sleepTimerChapterStopTime ?? 0) - (getCurrentTime() ?? 0) - if timeUntilChapterEnd <= 0 { - paused = true - remainingSleepTime = nil - } else { - remainingSleepTime = Int(timeUntilChapterEnd.rounded()) - } - } else { - if remainingSleepTime! <= 0 { - paused = true - } - remainingSleepTime! -= 1 + // MARK: - Helper logic + + private static func cleanupOldSessions(currentSessionId: String?) { + let realm = try! Realm() + let oldSessions = realm.objects(PlaybackSession.self) .where({ $0.isActiveSession == true }) + try! realm.write { + for s in oldSessions { + if s.id != currentSessionId { + s.isActiveSession = false } } } - - if listeningTimePassedSinceLastSync >= 5 { - syncPlayerProgress() - } - } - - public static func syncPlayerProgress() { - guard let player = player else { return } - guard player.isInitialized() else { return } - guard let session = getPlaybackSession() else { return } - - NSLog("Syncing player progress") - - // Get current time - let playerCurrentTime = player.getCurrentTime() - - // Prevent multiple sync requests - let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime - if (lastSyncTime > 0 && timeSinceLastSync < 1) { - NSLog("syncProgress last sync time was < 1 second so not syncing") - return - } - - // Prevent a sync if we got junk data from the player (occurs when exiting out of memory - guard !playerCurrentTime.isNaN else { return } - - lastSyncTime = Date().timeIntervalSince1970 // seconds - - session.update { - session.currentTime = playerCurrentTime - session.timeListening += listeningTimePassedSinceLastSync - session.updatedAt = Date().timeIntervalSince1970 * 1000 - } - listeningTimePassedSinceLastSync = 0 - - // Persist items in the database and sync to the server - if session.isLocal { Task { await PlayerProgress.shared.syncFromPlayer() } } - Task { await PlayerProgress.shared.syncToServer() } } @objc public static func syncServerProgressDuringPause() { diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 47251dee..1f76e0d7 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -13,11 +13,20 @@ class PlayerProgress { public static let shared = PlayerProgress() + private static let TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 10.0 + private init() {} - public func syncFromPlayer() async { + + // MARK: - SYNC HOOKS + + public func syncFromPlayer(currentTime: Double, includesPlayProgress: Bool, isStopping: Bool) async { let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromPlayer") + let session = await updateLocalSessionFromPlayer(currentTime: currentTime, includesPlayProgress: includesPlayProgress) updateLocalMediaProgressFromLocalSession() + if let session = session { + await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping) + } await UIApplication.shared.endBackgroundTask(backgroundToken) } @@ -33,8 +42,26 @@ class PlayerProgress { await UIApplication.shared.endBackgroundTask(backgroundToken) } - private func updateLocalSessionFromPlayer() async { + + // MARK: - SYNC LOGIC + + private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) async -> PlaybackSession? { + guard let session = PlayerHandler.getPlaybackSession() else { return nil } + let now = Date().timeIntervalSince1970 * 1000 + let lastUpdate = session.updatedAt ?? now + let timeSinceLastUpdate = now - lastUpdate + + session.update { + session.currentTime = currentTime + session.updatedAt = now + + if includesPlayProgress { + session.timeListening += timeSinceLastUpdate + } + } + + return session.freeze() } private func updateLocalMediaProgressFromLocalSession() { @@ -65,7 +92,19 @@ class PlayerProgress { } } - private func updateServerSessionFromLocalSession(_ session: PlaybackSession) async { + private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async { + // If required, rate limit requests based on session last update + if rateLimitSync { + let now = Date().timeIntervalSince1970 * 1000 + let lastUpdate = session.updatedAt ?? now + let timeSinceLastSync = now - lastUpdate + let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000 + guard timeSinceLastSync > timeBetweenSessionSync else { + // Skipping sync since last occurred within session sync time + return + } + } + NSLog("Sending sessionId(\(session.id)) to server") var success = false From ccecba7a19d34161223a6e9060bfb3f30789cbd3 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 22 Aug 2022 17:11:34 -0400 Subject: [PATCH 05/66] If adjusting sleep time from chapter, convert to regular sleep timer --- ios/App/Shared/player/PlayerHandler.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index 610b8f73..e552c6ef 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -101,10 +101,12 @@ class PlayerHandler { } public static func increaseSleepTime(increaseSeconds: Double) { + self.sleepTimerChapterStopTime = nil self.player?.increaseSleepTime(extraTimeInSeconds: increaseSeconds) } public static func decreaseSleepTime(decreaseSeconds: Double) { + self.sleepTimerChapterStopTime = nil self.player?.decreaseSleepTime(removeTimeInSeconds: decreaseSeconds) } From d084958f2db13d7abc2510fdea1bfa6b072f9098 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 22 Aug 2022 17:15:20 -0400 Subject: [PATCH 06/66] Scale time reporting by the rate --- ios/App/Shared/player/AudioPlayer.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index e0ae87b5..b6dfd315 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -132,16 +132,20 @@ class AudioPlayer: NSObject { } private func setupTimeObserver() { + removeTimeObserver() + let timeScale = CMTimeScale(NSEC_PER_SEC) - // Observe multiple times per seconds, as rate will be different depending on playback speed - let time = CMTime(seconds: 0.25, preferredTimescale: timeScale) - self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) { time in + // Rate will be different depending on playback speed, aim for 2 observations/sec + let seconds = 0.5 * (self.rate > 0 ? self.rate : 1.0) + let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale) + self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in + let sleepTimeStopAt = self?.sleepTimeStopAt Task { // Let the player update the current playback positions await PlayerProgress.shared.syncFromPlayer(currentTime: time.seconds, includesPlayProgress: true, isStopping: false) // Update the sleep time, if set - if self.sleepTimeStopAt != nil { + if sleepTimeStopAt != nil { NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) } } @@ -329,6 +333,9 @@ class AudioPlayer: NSObject { // If we have an active sleep timer, reschedule based on rate self.rescheduleSleepTimerAtTime(time: self.getCurrentTime(), secondsRemaining: sleepSecondsRemaining) + + // Setup the time observer again at the new rate + self.setupTimeObserver() } public func getSleepStopAt() -> Double? { From fe042f3f83a6e67566d61b89c4192d5aa13990a5 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 22 Aug 2022 17:27:55 -0400 Subject: [PATCH 07/66] Fix NaN causing bad data --- ios/App/Shared/player/PlayerProgress.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 1f76e0d7..f7f534bc 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -47,6 +47,7 @@ class PlayerProgress { private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) async -> PlaybackSession? { guard let session = PlayerHandler.getPlaybackSession() else { return nil } + guard !currentTime.isNaN else { return nil } // Prevent bad data on player stop let now = Date().timeIntervalSince1970 * 1000 let lastUpdate = session.updatedAt ?? now From 4f85827585712d8a0f3fc9ed78dffb9e4cb5bef2 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 22 Aug 2022 16:50:20 -0500 Subject: [PATCH 08/66] Add:Lock/unlock player ui setting #313, Update:Player track ui thinner with cursor --- components/app/AudioPlayer.vue | 67 +++++++++++++++++++++++----------- plugins/localStore.js | 18 +++++++++ 2 files changed, 64 insertions(+), 21 deletions(-) diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index 20ae55ae..4feb98b3 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -64,23 +64,24 @@
- first_page - {{ jumpBackwardsIcon }} + first_page + {{ jumpBackwardsIcon }}
{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}
- {{ jumpForwardIcon }} - last_page + {{ jumpForwardIcon }} + last_page
-
+
+

0:00

@@ -95,10 +96,18 @@ + @@ -147,6 +156,7 @@ export default { touchStartTime: 0, touchEndY: 0, useChapterTrack: false, + lockUi: false, isLoading: false, touchTrackStart: false, dragPercent: 0, @@ -162,17 +172,24 @@ export default { }, computed: { menuItems() { - var items = [] - items.push({ - text: 'Chapter Track', - value: 'chapter_track', - icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank' - }) - items.push({ - text: 'Close Player', - value: 'close', - icon: 'close' - }) + var items = [ + { + text: 'Chapter Track', + value: 'chapter_track', + icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank' + }, + { + text: 'Lock/Unlock', + value: 'lock', + icon: this.lockUi ? 'lock' : 'lock_open' + }, + { + text: 'Close Player', + value: 'close', + icon: 'close' + } + ] + return items }, jumpForwardIcon() { @@ -319,7 +336,7 @@ export default { } }, touchstartTrack(e) { - if (!e || !e.touches || !this.$refs.track || !this.showFullscreen) return + if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return this.touchTrackStart = true }, selectChapter(chapter) { @@ -469,6 +486,10 @@ export default { this.$refs.playedTrack.style.width = ptWidth + 'px' this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px' + if (this.$refs.trackCursor) { + this.$refs.trackCursor.style.left = ptWidth - 8 + 'px' + } + if (this.useChapterTrack) { if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px' if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px' @@ -496,7 +517,7 @@ export default { } }, clickTrack(e) { - if (this.isLoading) return + if (this.isLoading || this.lockUi) return if (!this.showFullscreen) { // Track not clickable on mini-player return @@ -622,7 +643,10 @@ export default { }, clickMenuAction(action) { this.showMoreMenuDialog = false - if (action === 'chapter_track') { + if (action === 'lock') { + this.lockUi = !this.lockUi + this.$localStore.setPlayerLock(this.lockUi) + } else if (action === 'chapter_track') { this.useChapterTrack = !this.useChapterTrack this.$nextTick(() => { @@ -715,6 +739,7 @@ export default { }, async init() { this.useChapterTrack = await this.$localStore.getUseChapterTrack() + this.lockUi = await this.$localStore.getPlayerLock() this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession) this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed) diff --git a/plugins/localStore.js b/plugins/localStore.js index b88e3411..b0ee0094 100644 --- a/plugins/localStore.js +++ b/plugins/localStore.js @@ -60,6 +60,24 @@ class LocalStorage { } } + async setPlayerLock(lock) { + try { + await Storage.set({ key: 'playerLock', value: lock ? '1' : '0' }) + } catch (error) { + console.error('[LocalStorage] Failed to set player lock', error) + } + } + + async getPlayerLock() { + try { + var obj = await Storage.get({ key: 'playerLock' }) || {} + return obj.value === '1' + } catch (error) { + console.error('[LocalStorage] Failed to get player lock', error) + return false + } + } + async setBookshelfListView(useIt) { try { await Storage.set({ key: 'bookshelfListView', value: useIt ? '1' : '0' }) From 2448b461f09344da1d6e116effaf4a8e79f69293 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 22 Aug 2022 18:00:37 -0400 Subject: [PATCH 09/66] Re-implement the paused timer --- ios/App/Shared/player/AudioPlayer.swift | 22 ++++++++++++++++++++++ ios/App/Shared/player/PlayerHandler.swift | 4 ---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index b6dfd315..5f9cbc18 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -44,6 +44,8 @@ class AudioPlayer: NSObject { private var currentTrackIndex = 0 private var allPlayerItems:[AVPlayerItem] = [] + private var pausedTimer: Timer? + // MARK: - Constructor init(sessionId: String, playWhenReady: Bool = false, playbackRate: Float = 1) { self.playWhenReady = playWhenReady @@ -88,6 +90,7 @@ class AudioPlayer: NSObject { NSLog("Audioplayer ready") } deinit { + self.stopPausedTimer() self.removeSleepTimer() self.removeTimeObserver() self.queueObserver?.invalidate() @@ -200,6 +203,21 @@ class AudioPlayer: NSObject { }) } + private func startPausedTimer() { + guard self.pausedTimer == nil else { return } + DispatchQueue.main.async { + self.pausedTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in + NSLog("PAUSE TIMER: Syncing from server") + Task { await PlayerProgress.shared.syncFromServer() } + } + } + } + + private func stopPausedTimer() { + self.pausedTimer?.invalidate() + self.pausedTimer = nil + } + // MARK: - Methods public func play(allowSeekBack: Bool = false) { if allowSeekBack { @@ -228,6 +246,8 @@ class AudioPlayer: NSObject { } lastPlayTime = Date.timeIntervalSinceReferenceDate + self.stopPausedTimer() + Task { let isPlaying = self.status > 0 await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: isPlaying, isStopping: false) @@ -252,6 +272,8 @@ class AudioPlayer: NSObject { updateNowPlaying() lastPlayTime = Date.timeIntervalSinceReferenceDate + + self.startPausedTimer() } public func seek(_ to: Double, from: String) { diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index e552c6ef..ab4ea126 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -168,8 +168,4 @@ class PlayerHandler { } } } - - @objc public static func syncServerProgressDuringPause() { - Task { await PlayerProgress.shared.syncFromServer() } - } } From 06f87d24a707720b5c8796b7f1ccf64de792c1bd Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 22 Aug 2022 18:12:00 -0400 Subject: [PATCH 10/66] Fix merge conflict --- ios/App/Shared/player/PlayerProgress.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 6b86861f..017cf9e3 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -123,7 +123,7 @@ class PlayerProgress { } } - private static func updateLocalSessionFromServerMediaProgress() async { + private func updateLocalSessionFromServerMediaProgress() async { NSLog("updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server") guard let session = try! await Realm().objects(PlaybackSession.self).last(where: { $0.isActiveSession == true })?.freeze() else { NSLog("updateLocalSessionFromServerMediaProgress: Failed to get session") From 94e261d7bfe47c181eee6dcff5966b176bde793e Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 22 Aug 2022 18:12:07 -0400 Subject: [PATCH 11/66] Fix bad data being encoded --- ios/App/Shared/models/local/LocalMediaProgress.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ios/App/Shared/models/local/LocalMediaProgress.swift b/ios/App/Shared/models/local/LocalMediaProgress.swift index 8962673d..6d9a475b 100644 --- a/ios/App/Shared/models/local/LocalMediaProgress.swift +++ b/ios/App/Shared/models/local/LocalMediaProgress.swift @@ -63,8 +63,12 @@ class LocalMediaProgress: Object, Codable { try container.encode(localLibraryItemId, forKey: .localLibraryItemId) try container.encode(localEpisodeId, forKey: .localEpisodeId) try container.encode(duration, forKey: .duration) - try container.encode(progress, forKey: .progress) - try container.encode(currentTime, forKey: .currentTime) + if progress.isNaN == false { + try container.encode(progress, forKey: .progress) + } + if currentTime.isNaN == false { + try container.encode(currentTime, forKey: .currentTime) + } try container.encode(isFinished, forKey: .isFinished) try container.encode(lastUpdate, forKey: .lastUpdate) try container.encode(startedAt, forKey: .startedAt) From 8bd0e863ae4a82a245b87798fd0b76582bdf23b1 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 22 Aug 2022 17:18:46 -0500 Subject: [PATCH 12/66] Fix:iOS audio player in landscape mode --- components/app/AudioPlayer.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index 4feb98b3..36eba3f7 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -772,8 +772,12 @@ export default { mounted() { this.updateScreenSize() if (screen.orientation) { + // Not available on ios screen.orientation.addEventListener('change', this.screenOrientationChange) + } else { + document.addEventListener('orientationchange', this.screenOrientationChange) } + window.addEventListener('resize', this.screenOrientationChange) this.$eventBus.$on('minimize-player', this.minimizePlayerEvt) document.body.addEventListener('touchstart', this.touchstart) @@ -783,8 +787,12 @@ export default { }, beforeDestroy() { if (screen.orientation) { + // Not available on ios screen.orientation.removeEventListener('change', this.screenOrientationChange) + } else { + document.removeEventListener('orientationchange', this.screenOrientationChange) } + window.removeEventListener('resize', this.screenOrientationChange) if (this.playbackSession) { console.log('[AudioPlayer] Before destroy closing playback') From 15cdff5aa2140e8056c9694c0b06b1f59275b5bc Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 22 Aug 2022 20:36:15 -0400 Subject: [PATCH 13/66] Fix typo in comment --- ios/App/Shared/player/AudioPlayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 15e1f81f..4037e4fe 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -334,7 +334,7 @@ class AudioPlayer: NSObject { self?.updateNowPlaying() // If we have an active sleep timer, reschedule based on seek, since seek is fuzzy - // Theis needs to occur after play() to capture the correct rate + // This needs to occur after play() to capture the correct playback rate if let currentTime = self?.getCurrentTime() { self?.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining) } From 6a885b7241adcbd0b389aff1e0439ae7bf23927e Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 23 Aug 2022 17:13:43 -0400 Subject: [PATCH 14/66] Fix session time using milliseconds instead of seconds --- ios/App/Shared/player/PlayerProgress.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 017cf9e3..8dcf7a35 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -49,16 +49,18 @@ class PlayerProgress { guard let session = PlayerHandler.getPlaybackSession() else { return nil } guard !currentTime.isNaN else { return nil } // Prevent bad data on player stop - let now = Date().timeIntervalSince1970 * 1000 - let lastUpdate = session.updatedAt ?? now - let timeSinceLastUpdate = now - lastUpdate + let nowInSeconds = Date().timeIntervalSince1970 + let nowInMilliseconds = nowInSeconds * 1000 + let lastUpdateInMilliseconds = session.updatedAt ?? nowInMilliseconds + let lastUpdateInSeconds = lastUpdateInMilliseconds / 1000 + let secondsSinceLastUpdate = nowInSeconds - lastUpdateInSeconds session.update { session.currentTime = currentTime - session.updatedAt = now + session.updatedAt = nowInMilliseconds if includesPlayProgress { - session.timeListening += timeSinceLastUpdate + session.timeListening += secondsSinceLastUpdate } } From 099be648bfdc25722d139ac71dafee0928741641 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 23 Aug 2022 17:32:43 -0400 Subject: [PATCH 15/66] Fix server not sending updates every 10 seconds --- ios/App/App/AppDelegate.swift | 2 +- ios/App/Shared/models/PlaybackSession.swift | 1 + ios/App/Shared/player/PlayerProgress.swift | 23 ++++++++++++++------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index 99d44fe4..6e93aab0 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -11,7 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. let configuration = Realm.Configuration( - schemaVersion: 2, + schemaVersion: 3, migrationBlock: { migration, oldSchemaVersion in if (oldSchemaVersion < 1) { NSLog("Realm schema version was \(oldSchemaVersion)") diff --git a/ios/App/Shared/models/PlaybackSession.swift b/ios/App/Shared/models/PlaybackSession.swift index fc562c4e..62db00cf 100644 --- a/ios/App/Shared/models/PlaybackSession.swift +++ b/ios/App/Shared/models/PlaybackSession.swift @@ -31,6 +31,7 @@ class PlaybackSession: Object, Codable, Deletable { @Persisted var serverConnectionConfigId: String? @Persisted var serverAddress: String? @Persisted var isActiveSession = true + @Persisted var serverUpdatedAt: Double = 0 var isLocal: Bool { self.localLibraryItem != nil } var mediaPlayer: String { "AVPlayer" } diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 8dcf7a35..4818dad5 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -96,11 +96,12 @@ class PlayerProgress { } private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async { + let nowInMilliseconds = Date().timeIntervalSince1970 * 1000 + // If required, rate limit requests based on session last update if rateLimitSync { - let now = Date().timeIntervalSince1970 * 1000 - let lastUpdate = session.updatedAt ?? now - let timeSinceLastSync = now - lastUpdate + let lastUpdateInMilliseconds = session.serverUpdatedAt + let timeSinceLastSync = nowInMilliseconds - lastUpdateInMilliseconds let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000 guard timeSinceLastSync > timeBetweenSessionSync else { // Skipping sync since last occurred within session sync time @@ -118,10 +119,18 @@ class PlayerProgress { success = await ApiClient.reportPlaybackProgress(report: playbackReport, sessionId: session.id) } - // Remove old sessions after they synced with the server - if success && !session.isActiveSession { - NSLog("Deleting sessionId(\(session.id)) as is no longer active") - session.thaw()?.delete() + if success { + if let session = session.thaw() { + // Update the server sync time, which is different than lastUpdate + session.update { + session.serverUpdatedAt = nowInMilliseconds + } + + // Remove old sessions after they synced with the server + if !session.isActiveSession { + session.delete() + } + } } } From d5f39e5cb12f1fed8e68b3b5bc8d652d2279ab37 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 23 Aug 2022 18:22:11 -0400 Subject: [PATCH 16/66] Fix async call not waiting for results --- ios/App/Shared/player/PlayerProgress.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 4818dad5..81c85161 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -32,7 +32,7 @@ class PlayerProgress { public func syncToServer() async { let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncToServer") - updateAllServerSessionFromLocalSession() + await updateAllServerSessionFromLocalSession() await UIApplication.shared.endBackgroundTask(backgroundToken) } @@ -87,11 +87,15 @@ class PlayerProgress { NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil) } - private func updateAllServerSessionFromLocalSession() { - let sessions = try! Realm().objects(PlaybackSession.self).where({ $0.serverConnectionConfigId == Store.serverConfig?.id }) - for session in sessions { - let session = session.freeze() - Task { await updateServerSessionFromLocalSession(session) } + private func updateAllServerSessionFromLocalSession() async { + await withTaskGroup(of: Void.self) { [self] group in + for session in try! await Realm().objects(PlaybackSession.self).where({ $0.serverConnectionConfigId == Store.serverConfig?.id }) { + let session = session.freeze() + group.addTask { + await self.updateServerSessionFromLocalSession(session) + } + } + await group.waitForAll() } } From 2a2ebefeb987554a4aae7f30f373777503bc17e1 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 23 Aug 2022 18:51:30 -0400 Subject: [PATCH 17/66] Fix thread safety issues --- ios/App/Shared/player/PlayerProgress.swift | 56 ++++++++++++---------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 81c85161..fd81da0b 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -49,13 +49,15 @@ class PlayerProgress { guard let session = PlayerHandler.getPlaybackSession() else { return nil } guard !currentTime.isNaN else { return nil } // Prevent bad data on player stop - let nowInSeconds = Date().timeIntervalSince1970 - let nowInMilliseconds = nowInSeconds * 1000 - let lastUpdateInMilliseconds = session.updatedAt ?? nowInMilliseconds - let lastUpdateInSeconds = lastUpdateInMilliseconds / 1000 - let secondsSinceLastUpdate = nowInSeconds - lastUpdateInSeconds - session.update { + session.realm?.refresh() + + let nowInSeconds = Date().timeIntervalSince1970 + let nowInMilliseconds = nowInSeconds * 1000 + let lastUpdateInMilliseconds = session.updatedAt ?? nowInMilliseconds + let lastUpdateInSeconds = lastUpdateInMilliseconds / 1000 + let secondsSinceLastUpdate = nowInSeconds - lastUpdateInSeconds + session.currentTime = currentTime session.updatedAt = nowInMilliseconds @@ -100,18 +102,31 @@ class PlayerProgress { } private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async { - let nowInMilliseconds = Date().timeIntervalSince1970 * 1000 + guard var session = session.thaw() else { return } + var safeToSync = true - // If required, rate limit requests based on session last update - if rateLimitSync { + // We need to update and check the server time in a transaction for thread-safety + session.update { + session.realm?.refresh() + + let nowInMilliseconds = Date().timeIntervalSince1970 * 1000 let lastUpdateInMilliseconds = session.serverUpdatedAt - let timeSinceLastSync = nowInMilliseconds - lastUpdateInMilliseconds - let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000 - guard timeSinceLastSync > timeBetweenSessionSync else { - // Skipping sync since last occurred within session sync time - return + + // If required, rate limit requests based on session last update + if rateLimitSync { + let timeSinceLastSync = nowInMilliseconds - lastUpdateInMilliseconds + let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000 + safeToSync = timeSinceLastSync > timeBetweenSessionSync + if !safeToSync { + return // This only exits the update block + } } + + session.serverUpdatedAt = nowInMilliseconds } + session = session.freeze() + + guard safeToSync else { return } NSLog("Sending sessionId(\(session.id)) to server") @@ -123,17 +138,10 @@ class PlayerProgress { success = await ApiClient.reportPlaybackProgress(report: playbackReport, sessionId: session.id) } - if success { + // Remove old sessions after they synced with the server + if success && !session.isActiveSession { if let session = session.thaw() { - // Update the server sync time, which is different than lastUpdate - session.update { - session.serverUpdatedAt = nowInMilliseconds - } - - // Remove old sessions after they synced with the server - if !session.isActiveSession { - session.delete() - } + session.delete() } } } From 10ddc1c9117c6df6a79a274cd63b56fe77a8849e Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 23 Aug 2022 18:56:08 -0400 Subject: [PATCH 18/66] Fix server config not factored in determining active session --- ios/App/App/plugins/AbsAudioPlayer.swift | 4 +++- ios/App/Shared/player/PlayerHandler.swift | 4 +++- ios/App/Shared/player/PlayerProgress.swift | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ios/App/App/plugins/AbsAudioPlayer.swift b/ios/App/App/plugins/AbsAudioPlayer.swift index c376d66a..2b4197fc 100644 --- a/ios/App/App/plugins/AbsAudioPlayer.swift +++ b/ios/App/App/plugins/AbsAudioPlayer.swift @@ -38,7 +38,9 @@ public class AbsAudioPlayer: CAPPlugin { do { // Fetch the most recent active session - let activeSession = try await Realm().objects(PlaybackSession.self).where({ $0.isActiveSession == true }).last?.freeze() + let activeSession = try await Realm().objects(PlaybackSession.self).where({ + $0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id + }).last?.freeze() if let activeSession = activeSession { await PlayerProgress.shared.syncFromServer() try self.startPlaybackSession(activeSession, playWhenReady: false, playbackRate: PlayerSettings.main().playbackRate) diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index ab4ea126..0a330f61 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -159,7 +159,9 @@ class PlayerHandler { private static func cleanupOldSessions(currentSessionId: String?) { let realm = try! Realm() - let oldSessions = realm.objects(PlaybackSession.self) .where({ $0.isActiveSession == true }) + let oldSessions = realm.objects(PlaybackSession.self) .where({ + $0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id + }) try! realm.write { for s in oldSessions { if s.id != currentSessionId { diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index fd81da0b..1a8ed23c 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -148,7 +148,9 @@ class PlayerProgress { private func updateLocalSessionFromServerMediaProgress() async { NSLog("updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server") - guard let session = try! await Realm().objects(PlaybackSession.self).last(where: { $0.isActiveSession == true })?.freeze() else { + guard let session = try! await Realm().objects(PlaybackSession.self).last(where: { + $0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id + })?.freeze() else { NSLog("updateLocalSessionFromServerMediaProgress: Failed to get session") return } From 67a6aec1325c076b0cea4a81f292ec5ac5d9f83f Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 23 Aug 2022 22:06:41 -0400 Subject: [PATCH 19/66] Move AudioPlayer tasks off the main queue --- ios/App/Shared/player/AudioPlayer.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 4037e4fe..6f41a2a7 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -18,6 +18,8 @@ enum PlayMethod:Int { } class AudioPlayer: NSObject { + private let audioPlayerQueue = DispatchQueue(label: "ABSAudioPlayerQueue") + // enums and @objc are not compatible @objc dynamic var status: Int @objc dynamic var rate: Float @@ -141,7 +143,7 @@ class AudioPlayer: NSObject { // Rate will be different depending on playback speed, aim for 2 observations/sec let seconds = 0.5 * (self.rate > 0 ? self.rate : 1.0) let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale) - self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in + self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: audioPlayerQueue) { [weak self] time in let sleepTimeStopAt = self?.sleepTimeStopAt Task { // Let the player update the current playback positions @@ -205,7 +207,7 @@ class AudioPlayer: NSObject { private func startPausedTimer() { guard self.pausedTimer == nil else { return } - DispatchQueue.main.async { + audioPlayerQueue.async { self.pausedTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in NSLog("PAUSE TIMER: Syncing from server") Task { await PlayerProgress.shared.syncFromServer() } @@ -398,7 +400,7 @@ class AudioPlayer: NSObject { var times = [NSValue]() times.append(NSValue(time: sleepTime)) - sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: .main) { [weak self] in + sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: audioPlayerQueue) { [weak self] in NSLog("SLEEP TIMER: Pausing audio") self?.pause() self?.removeSleepTimer() From 452b25057e3c82cf97a443b6aef133fc320c8b72 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 23 Aug 2022 22:07:07 -0400 Subject: [PATCH 20/66] Use the same Realm instead for updates of LocalMediaProgress --- ios/App/Shared/models/local/LocalMediaProgress.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/App/Shared/models/local/LocalMediaProgress.swift b/ios/App/Shared/models/local/LocalMediaProgress.swift index 6d9a475b..001e07ff 100644 --- a/ios/App/Shared/models/local/LocalMediaProgress.swift +++ b/ios/App/Shared/models/local/LocalMediaProgress.swift @@ -120,7 +120,7 @@ extension LocalMediaProgress { } func updateIsFinished(_ finished: Bool) { - try! Realm().write { + try! self.realm?.write { if self.isFinished != finished { self.progress = finished ? 1.0 : 0.0 } @@ -136,7 +136,7 @@ extension LocalMediaProgress { } func updateFromPlaybackSession(_ playbackSession: PlaybackSession) { - try! Realm().write { + try! self.realm?.write { self.currentTime = playbackSession.currentTime self.progress = playbackSession.progress self.lastUpdate = Date().timeIntervalSince1970 * 1000 @@ -146,7 +146,7 @@ extension LocalMediaProgress { } func updateFromServerMediaProgress(_ serverMediaProgress: MediaProgress) { - try! Realm().write { + try! self.realm?.write { self.isFinished = serverMediaProgress.isFinished self.progress = serverMediaProgress.progress self.currentTime = serverMediaProgress.currentTime From 46623d70a355f730c10bc9d7fc8c538eeab567f8 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 23 Aug 2022 22:37:28 -0400 Subject: [PATCH 21/66] Realm modifications should occur on concurrent queue --- ios/App/Shared/player/AudioPlayer.swift | 8 +- ios/App/Shared/player/PlayerProgress.swift | 144 +++++++++++---------- 2 files changed, 80 insertions(+), 72 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 6f41a2a7..1f19feb6 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -18,8 +18,6 @@ enum PlayMethod:Int { } class AudioPlayer: NSObject { - private let audioPlayerQueue = DispatchQueue(label: "ABSAudioPlayerQueue") - // enums and @objc are not compatible @objc dynamic var status: Int @objc dynamic var rate: Float @@ -143,7 +141,7 @@ class AudioPlayer: NSObject { // Rate will be different depending on playback speed, aim for 2 observations/sec let seconds = 0.5 * (self.rate > 0 ? self.rate : 1.0) let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale) - self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: audioPlayerQueue) { [weak self] time in + self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: PlayerProgress.queue) { [weak self] time in let sleepTimeStopAt = self?.sleepTimeStopAt Task { // Let the player update the current playback positions @@ -207,7 +205,7 @@ class AudioPlayer: NSObject { private func startPausedTimer() { guard self.pausedTimer == nil else { return } - audioPlayerQueue.async { + PlayerProgress.queue.async { self.pausedTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in NSLog("PAUSE TIMER: Syncing from server") Task { await PlayerProgress.shared.syncFromServer() } @@ -400,7 +398,7 @@ class AudioPlayer: NSObject { var times = [NSValue]() times.append(NSValue(time: sleepTime)) - sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: audioPlayerQueue) { [weak self] in + sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: PlayerProgress.queue) { [weak self] in NSLog("SLEEP TIMER: Pausing audio") self?.pause() self?.removeSleepTimer() diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 1a8ed23c..231ad102 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -10,8 +10,8 @@ import UIKit import RealmSwift class PlayerProgress { - public static let shared = PlayerProgress() + public static let queue = DispatchQueue(label: "ABSPlayerProgressQueue") private static let TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 10.0 @@ -22,7 +22,7 @@ class PlayerProgress { public func syncFromPlayer(currentTime: Double, includesPlayProgress: Bool, isStopping: Bool) async { let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromPlayer") - let session = await updateLocalSessionFromPlayer(currentTime: currentTime, includesPlayProgress: includesPlayProgress) + let session = updateLocalSessionFromPlayer(currentTime: currentTime, includesPlayProgress: includesPlayProgress) updateLocalMediaProgressFromLocalSession() if let session = session { await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping) @@ -45,48 +45,52 @@ class PlayerProgress { // MARK: - SYNC LOGIC - private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) async -> PlaybackSession? { - guard let session = PlayerHandler.getPlaybackSession() else { return nil } - guard !currentTime.isNaN else { return nil } // Prevent bad data on player stop - - session.update { - session.realm?.refresh() + private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) -> PlaybackSession? { + PlayerProgress.queue.sync { + guard let session = PlayerHandler.getPlaybackSession() else { return nil } + guard !currentTime.isNaN else { return nil } // Prevent bad data on player stop - let nowInSeconds = Date().timeIntervalSince1970 - let nowInMilliseconds = nowInSeconds * 1000 - let lastUpdateInMilliseconds = session.updatedAt ?? nowInMilliseconds - let lastUpdateInSeconds = lastUpdateInMilliseconds / 1000 - let secondsSinceLastUpdate = nowInSeconds - lastUpdateInSeconds - - session.currentTime = currentTime - session.updatedAt = nowInMilliseconds - - if includesPlayProgress { - session.timeListening += secondsSinceLastUpdate + session.update { + session.realm?.refresh() + + let nowInSeconds = Date().timeIntervalSince1970 + let nowInMilliseconds = nowInSeconds * 1000 + let lastUpdateInMilliseconds = session.updatedAt ?? nowInMilliseconds + let lastUpdateInSeconds = lastUpdateInMilliseconds / 1000 + let secondsSinceLastUpdate = nowInSeconds - lastUpdateInSeconds + + session.currentTime = currentTime + session.updatedAt = nowInMilliseconds + + if includesPlayProgress { + session.timeListening += secondsSinceLastUpdate + } } + + return session.freeze() } - - return session.freeze() } private func updateLocalMediaProgressFromLocalSession() { - guard let session = PlayerHandler.getPlaybackSession() else { return } - guard session.isLocal else { return } - - let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId) - guard let localMediaProgress = localMediaProgress else { - // Local media progress should have been created - // If we're here, it means a library id is invalid - return - } + PlayerProgress.queue.sync { + guard let session = PlayerHandler.getPlaybackSession() else { return } + guard session.isLocal else { return } + + let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId) + guard let localMediaProgress = localMediaProgress else { + // Local media progress should have been created + // If we're here, it means a library id is invalid + return + } - localMediaProgress.updateFromPlaybackSession(session) - Database.shared.saveLocalMediaProgress(localMediaProgress) - - NSLog("Local progress saved to the database") - - // Send the local progress back to front-end - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil) + localMediaProgress.updateFromPlaybackSession(session) + Database.shared.saveLocalMediaProgress(localMediaProgress) + + NSLog("Local progress saved to the database") + + // Send the local progress back to front-end + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil) + } } private func updateAllServerSessionFromLocalSession() async { @@ -102,31 +106,32 @@ class PlayerProgress { } private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async { - guard var session = session.thaw() else { return } - var safeToSync = true - - // We need to update and check the server time in a transaction for thread-safety - session.update { - session.realm?.refresh() + PlayerProgress.queue.sync { + var safeToSync = true + guard var session = session.thaw() else { return } - let nowInMilliseconds = Date().timeIntervalSince1970 * 1000 - let lastUpdateInMilliseconds = session.serverUpdatedAt - - // If required, rate limit requests based on session last update - if rateLimitSync { - let timeSinceLastSync = nowInMilliseconds - lastUpdateInMilliseconds - let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000 - safeToSync = timeSinceLastSync > timeBetweenSessionSync - if !safeToSync { - return // This only exits the update block + // We need to update and check the server time in a transaction for thread-safety + session.update { + session.realm?.refresh() + + let nowInMilliseconds = Date().timeIntervalSince1970 * 1000 + let lastUpdateInMilliseconds = session.serverUpdatedAt + + // If required, rate limit requests based on session last update + if rateLimitSync { + let timeSinceLastSync = nowInMilliseconds - lastUpdateInMilliseconds + let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000 + safeToSync = timeSinceLastSync > timeBetweenSessionSync + if !safeToSync { + return // This only exits the update block + } } + + session.serverUpdatedAt = nowInMilliseconds } - - session.serverUpdatedAt = nowInMilliseconds + session = session.freeze() + guard safeToSync else { return } } - session = session.freeze() - - guard safeToSync else { return } NSLog("Sending sessionId(\(session.id)) to server") @@ -138,10 +143,13 @@ class PlayerProgress { success = await ApiClient.reportPlaybackProgress(report: playbackReport, sessionId: session.id) } + // Remove old sessions after they synced with the server if success && !session.isActiveSession { - if let session = session.thaw() { - session.delete() + PlayerProgress.queue.sync { + if let session = session.thaw() { + session.delete() + } } } } @@ -176,14 +184,16 @@ class PlayerProgress { // Update the session, if needed if serverIsNewerThanLocal && currentTimeIsDifferent { - NSLog("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)") - guard let session = session.thaw() else { return } - session.update { - session.currentTime = serverCurrentTime - session.updatedAt = serverLastUpdate + PlayerProgress.queue.sync { + NSLog("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)") + guard let session = session.thaw() else { return } + session.update { + session.currentTime = serverCurrentTime + session.updatedAt = serverLastUpdate + } + NSLog("updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)") + PlayerHandler.seek(amount: session.currentTime) } - NSLog("updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)") - PlayerHandler.seek(amount: session.currentTime) } else { NSLog("updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress") } From bba388aefa5091536a8f4dd6ce59a661ae43ac13 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 24 Aug 2022 17:55:07 -0500 Subject: [PATCH 22/66] Update:Android auto show in progress media items across all libraries #285 --- .../audiobookshelf/app/data/DataClasses.kt | 125 +++++++---------- .../audiobookshelf/app/data/DeviceClasses.kt | 6 +- .../audiobookshelf/app/data/ItemInProgress.kt | 34 +++++ .../audiobookshelf/app/data/LibraryItem.kt | 90 ++++++++++++ .../app/data/LocalLibraryItem.kt | 48 +++++-- .../audiobookshelf/app/media/MediaManager.kt | 132 ++++-------------- .../audiobookshelf/app/player/BrowseTree.kt | 46 ++---- .../app/player/PlayerNotificationService.kt | 73 ++++++---- .../audiobookshelf/app/server/ApiHandler.kt | 19 +-- 9 files changed, 295 insertions(+), 278 deletions(-) create mode 100644 android/app/src/main/java/com/audiobookshelf/app/data/ItemInProgress.kt create mode 100644 android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt index 9e4997f5..807457e6 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt @@ -1,69 +1,11 @@ package com.audiobookshelf.app.data -import android.net.Uri +import android.os.Bundle +import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaMetadataCompat -import com.audiobookshelf.app.R -import com.audiobookshelf.app.device.DeviceManager +import androidx.media.utils.MediaConstants import com.fasterxml.jackson.annotation.* -@JsonIgnoreProperties(ignoreUnknown = true) -data class LibraryItem( - var id:String, - var ino:String, - var libraryId:String, - var folderId:String, - var path:String, - var relPath:String, - var mtimeMs:Long, - var ctimeMs:Long, - var birthtimeMs:Long, - var addedAt:Long, - var updatedAt:Long, - var lastScan:Long?, - var scanVersion:String?, - var isMissing:Boolean, - var isInvalid:Boolean, - var mediaType:String, - var media:MediaType, - var libraryFiles:MutableList?, - var userMediaProgress:MediaProgress? // Only included when requesting library item with progress (for downloads) -) : LibraryItemWrapper() { - @get:JsonIgnore - val title get() = media.metadata.title - @get:JsonIgnore - val authorName get() = media.metadata.getAuthorDisplayName() - - @JsonIgnore - fun getCoverUri():Uri { - if (media.coverPath == null) { - return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon) - } - - return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}") - } - - @JsonIgnore - fun checkHasTracks():Boolean { - return if (mediaType == "podcast") { - ((media as Podcast).numEpisodes ?: 0) > 0 - } else { - ((media as Book).numTracks ?: 0) > 0 - } - } - - @JsonIgnore - fun getMediaMetadata(): MediaMetadataCompat { - return MediaMetadataCompat.Builder().apply { - putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) - putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) - putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) - putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName) - putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString()) - putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName) - }.build() - } -} - // This auto-detects whether it is a Book or Podcast @JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION) @JsonSubTypes( @@ -121,7 +63,7 @@ class Podcast( } @JsonIgnore override fun addAudioTrack(audioTrack:AudioTrack) { - val newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null) + val newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,(episodes?.size ?: 0) + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null) episodes?.add(newEpisode) var index = 1 @@ -142,7 +84,7 @@ class Podcast( } @JsonIgnore fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode { - val newEpisode = PodcastEpisode("local_" + episode.id,episodes?.size ?: 0 + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id) + val newEpisode = PodcastEpisode("local_" + episode.id,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id) episodes?.add(newEpisode) var index = 1 @@ -293,24 +235,51 @@ data class PodcastEpisode( var serverEpisodeId:String? // For local podcasts to match with server podcasts ) { @JsonIgnore - fun getMediaMetadata(libraryItem:LibraryItemWrapper): MediaMetadataCompat { - var coverUri:Uri = Uri.EMPTY - val podcast = if(libraryItem is LocalLibraryItem) { - coverUri = libraryItem.getCoverUri() - libraryItem.media as Podcast + fun getMediaDescription(libraryItem:LibraryItemWrapper, progress:MediaProgressWrapper?): MediaDescriptionCompat { + val coverUri = if(libraryItem is LocalLibraryItem) { + libraryItem.getCoverUri() } else { - coverUri = (libraryItem as LibraryItem).getCoverUri() - (libraryItem as LibraryItem).media as Podcast + (libraryItem as LibraryItem).getCoverUri() } - return MediaMetadataCompat.Builder().apply { - putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) - putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) - putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) - putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName()) - putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString()) - - }.build() + val extras = Bundle() + if (progress != null) { + if (progress.isFinished) { + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED + ) + } else { + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED + ) + extras.putDouble( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress + ) + } + } else { + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED + ) + } +// return MediaMetadataCompat.Builder().apply { +// putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) +// putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) +// putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) +// putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName()) +// putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString()) +// +// }.build() + val libraryItemDescription = libraryItem.getMediaDescription(null) + return MediaDescriptionCompat.Builder() + .setMediaId(id) + .setTitle(title) + .setIconUri(coverUri) + .setSubtitle(libraryItemDescription.title) + .setExtras(extras) + .build() } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt index b81c67da..ce13089d 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt @@ -1,5 +1,6 @@ package com.audiobookshelf.app.data +import android.support.v4.media.MediaDescriptionCompat import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonSubTypes @@ -92,7 +93,10 @@ data class LocalFolder( JsonSubTypes.Type(LibraryItem::class), JsonSubTypes.Type(LocalLibraryItem::class) ) -open class LibraryItemWrapper() +open class LibraryItemWrapper(var id:String) { + @JsonIgnore + open fun getMediaDescription(progress:MediaProgressWrapper?): MediaDescriptionCompat { return MediaDescriptionCompat.Builder().build() } +} @JsonIgnoreProperties(ignoreUnknown = true) data class DeviceInfo( diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/ItemInProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/ItemInProgress.kt new file mode 100644 index 00000000..48dd51fb --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/ItemInProgress.kt @@ -0,0 +1,34 @@ +/* + Used in Android Auto to represent a podcast episode or an audiobook in progress + */ + +package com.audiobookshelf.app.data + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.core.json.JsonReadFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.json.JSONObject + +@JsonIgnoreProperties(ignoreUnknown = true) +data class ItemInProgress( + val libraryItemWrapper: LibraryItemWrapper, + val episode: PodcastEpisode?, + val progressLastUpdate: Long, + val isLocal: Boolean +) { + companion object { + fun makeFromServerObject(serverItem: JSONObject):ItemInProgress { + val jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) + val libraryItem = jacksonMapper.readValue(serverItem.toString()) + + var episode:PodcastEpisode? = null + if (serverItem.has("recentEpisode")) { + episode = jacksonMapper.readValue(serverItem.get("recentEpisode").toString()) + } + + val progressLastUpdate:Long = serverItem.getLong("progressLastUpdate") + return ItemInProgress(libraryItem, episode, progressLastUpdate, false) + } + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt new file mode 100644 index 00000000..4166a6e3 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt @@ -0,0 +1,90 @@ +package com.audiobookshelf.app.data + +import android.net.Uri +import android.os.Bundle +import android.support.v4.media.MediaDescriptionCompat +import androidx.media.utils.MediaConstants +import com.audiobookshelf.app.R +import com.audiobookshelf.app.device.DeviceManager +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +class LibraryItem( + id:String, + var ino:String, + var libraryId:String, + var folderId:String, + var path:String, + var relPath:String, + var mtimeMs:Long, + var ctimeMs:Long, + var birthtimeMs:Long, + var addedAt:Long, + var updatedAt:Long, + var lastScan:Long?, + var scanVersion:String?, + var isMissing:Boolean, + var isInvalid:Boolean, + var mediaType:String, + var media:MediaType, + var libraryFiles:MutableList?, + var userMediaProgress:MediaProgress? // Only included when requesting library item with progress (for downloads) +) : LibraryItemWrapper(id) { + @get:JsonIgnore + val title get() = media.metadata.title + @get:JsonIgnore + val authorName get() = media.metadata.getAuthorDisplayName() + + @JsonIgnore + fun getCoverUri(): Uri { + if (media.coverPath == null) { + return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon) + } + + return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}") + } + + @JsonIgnore + fun checkHasTracks():Boolean { + return if (mediaType == "podcast") { + ((media as Podcast).numEpisodes ?: 0) > 0 + } else { + ((media as Book).numTracks ?: 0) > 0 + } + } + + @JsonIgnore + override fun getMediaDescription(progress:MediaProgressWrapper?): MediaDescriptionCompat { + val extras = Bundle() + if (progress != null) { + if (progress.isFinished) { + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED + ) + } else { + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED + ) + extras.putDouble( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress + ) + } + } else if (mediaType != "podcast") { + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED + ) + } + + return MediaDescriptionCompat.Builder() + .setMediaId(id) + .setTitle(title) + .setIconUri(getCoverUri()) + .setSubtitle(authorName) + .setExtras(extras) + .build() + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt index c1b2e2ce..390cfd14 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt @@ -1,10 +1,8 @@ package com.audiobookshelf.app.data -import android.content.Context import android.net.Uri import android.os.Bundle import android.support.v4.media.MediaDescriptionCompat -import android.support.v4.media.MediaMetadataCompat import android.util.Log import androidx.media.utils.MediaConstants import com.audiobookshelf.app.R @@ -14,8 +12,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties import java.util.* @JsonIgnoreProperties(ignoreUnknown = true) -data class LocalLibraryItem( - var id:String, +class LocalLibraryItem( + id:String, var folderId:String, var basePath:String, var absolutePath:String, @@ -32,7 +30,7 @@ data class LocalLibraryItem( var serverAddress:String?, var serverUserId:String?, var libraryItemId:String? - ) : LibraryItemWrapper() { + ) : LibraryItemWrapper(id) { @get:JsonIgnore val title get() = media.metadata.title @get:JsonIgnore @@ -98,16 +96,38 @@ data class LocalLibraryItem( } @JsonIgnore - fun getMediaMetadata(): MediaMetadataCompat { + override fun getMediaDescription(progress:MediaProgressWrapper?): MediaDescriptionCompat { val coverUri = getCoverUri() - return MediaMetadataCompat.Builder().apply { - putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) - putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) - putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) - putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName) - putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString()) - putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName) - }.build() + val extras = Bundle() + if (progress != null) { + if (progress.isFinished) { + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED + ) + } else { + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED + ) + extras.putDouble( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress + ) + } + } else if (mediaType != "podcast") { + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED + ) + } + + return MediaDescriptionCompat.Builder() + .setMediaId(id) + .setTitle(title) + .setIconUri(coverUri) + .setSubtitle(authorName) + .setExtras(extras) + .build() } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt index 0ef698bb..3199e8e8 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt @@ -2,12 +2,8 @@ package com.audiobookshelf.app.media import android.app.Activity import android.content.Context -import android.os.Bundle import android.support.v4.media.MediaBrowserCompat -import android.support.v4.media.MediaDescriptionCompat -import android.support.v4.media.MediaMetadataCompat import android.util.Log -import androidx.media.utils.MediaConstants import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.server.ApiHandler @@ -26,11 +22,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { var selectedLibraryItems = mutableListOf() var selectedLibraryId = "" - var selectedLibraryItemWrapper:LibraryItemWrapper? = null var selectedPodcast:Podcast? = null var selectedLibraryItemId:String? = null - var serverPodcastEpisodes = listOf() + var podcastEpisodeLibraryItemMap = mutableMapOf() var serverLibraryCategories = listOf() + var serverItemsInProgress = listOf() var serverLibraries = listOf() var serverConfigIdUsed:String? = null var serverConfigLastPing:Long = 0L @@ -71,7 +67,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig() if (!DeviceManager.isConnectedToServer || !apiHandler.isOnline() || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) { - serverPodcastEpisodes = listOf() + podcastEpisodeLibraryItemMap = mutableMapOf() serverLibraryCategories = listOf() serverLibraries = listOf() serverLibraryItems = mutableListOf() @@ -80,13 +76,13 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { } } - fun loadLibraryCategories(libraryId:String, cb: (List) -> Unit) { - if (serverLibraryCategories.isNotEmpty()) { - cb(serverLibraryCategories) + fun loadItemsInProgressForAllLibraries(cb: (List) -> Unit) { + if (serverItemsInProgress.isNotEmpty()) { + cb(serverItemsInProgress) } else { - apiHandler.getLibraryCategories(libraryId) { - serverLibraryCategories = it - cb(it) + apiHandler.getAllItemsInProgress { itemsInProgress -> + serverItemsInProgress = itemsInProgress + cb(serverItemsInProgress) } } } @@ -129,44 +125,39 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { loadLibraryItem(libraryItemId) { libraryItemWrapper -> Log.d(tag, "Loaded Podcast library item $libraryItemWrapper") - selectedLibraryItemWrapper = libraryItemWrapper - libraryItemWrapper?.let { if (libraryItemWrapper is LocalLibraryItem) { // Local podcast episodes if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) { - serverPodcastEpisodes = listOf() cb(mutableListOf()) } else { val podcast = libraryItemWrapper.media as Podcast - serverPodcastEpisodes = podcast.episodes ?: listOf() selectedLibraryItemId = libraryItemWrapper.id selectedPodcast = podcast val children = podcast.episodes?.map { podcastEpisode -> Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}") - val mediaMetadata = podcastEpisode.getMediaMetadata(libraryItemWrapper) val progress = DeviceManager.dbManager.getLocalMediaProgress("${libraryItemWrapper.id}-${podcastEpisode.id}") - val description = getMediaDescriptionFromMediaMetadata(mediaMetadata, progress) + val description = podcastEpisode.getMediaDescription(libraryItemWrapper, progress) MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) } children?.let { cb(children as MutableList) } ?: cb(mutableListOf()) } } else if (libraryItemWrapper is LibraryItem) { // Server podcast episodes if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) { - serverPodcastEpisodes = listOf() cb(mutableListOf()) } else { val podcast = libraryItemWrapper.media as Podcast - serverPodcastEpisodes = podcast.episodes ?: listOf() + podcast.episodes?.forEach { podcastEpisode -> + podcastEpisodeLibraryItemMap[podcastEpisode.id] = LibraryItemWithEpisode(libraryItemWrapper, podcastEpisode) + } selectedLibraryItemId = libraryItemWrapper.id selectedPodcast = podcast val children = podcast.episodes?.map { podcastEpisode -> - val mediaMetadata = podcastEpisode.getMediaMetadata(libraryItemWrapper) val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id } - val description = getMediaDescriptionFromMediaMetadata(mediaMetadata, progress) + val description = podcastEpisode.getMediaDescription(libraryItemWrapper, progress) MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) } children?.let { cb(children as MutableList) } ?: cb(mutableListOf()) @@ -275,26 +266,8 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { } - // TODO: Load currently listening category for local items - fun loadLocalCategory():List { - val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book") - val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast") - val cats = mutableListOf() - if (localBooks.isNotEmpty()) { - cats.add(LibraryCategory("local-books", "Local Books", "book", localBooks, true)) - } - if (localPodcasts.isNotEmpty()) { - cats.add(LibraryCategory("local-podcasts", "Local Podcasts", "podcast", localPodcasts, true)) - } - return cats - } - - fun loadAndroidAutoItems(cb: (List) -> Unit) { + fun loadAndroidAutoItems(cb: () -> Unit) { Log.d(tag, "Load android auto items") - val cats = mutableListOf() - - val localCategories = loadLocalCategory() - cats.addAll(localCategories) // Check if any valid server connection if not use locally downloaded books checkSetValidServerConnectionConfig { isConnected -> @@ -304,40 +277,29 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { loadLibraries { libraries -> if (libraries.isEmpty()) { Log.w(tag, "No libraries returned from server request") - cb(cats) // Return download category only + cb() } else { val library = libraries[0] Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}") - loadLibraryCategories(library.id) { libraryCategories -> - - // Only using book or podcast library categories for now - libraryCategories.forEach { - - // Add items in continue listening to serverLibraryItems - if (it.id == "continue-listening") { - it.entities.forEach { libraryItemWrapper -> - val libraryItem = libraryItemWrapper as LibraryItem - if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) { - serverLibraryItems.add(libraryItem) - } - } + loadItemsInProgressForAllLibraries { itemsInProgress -> + itemsInProgress.forEach { + val libraryItem = it.libraryItemWrapper as LibraryItem + if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) { + serverLibraryItems.add(libraryItem) } - - // Log.d(tag, "Found library category ${it.label} with type ${it.type}") - if (it.type == library.mediaType) { - // Log.d(tag, "Using library category ${it.id}") - cats.add(it) + if (it.episode != null) { + podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode) } } - cb(cats) + cb() // Fully loaded } } } - } else { // Not connected/no internet sent downloaded cats only - cb(cats) + } else { // Not connected to server + cb() } } } @@ -355,12 +317,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { if (id.startsWith("local")) { return DeviceManager.dbManager.getLocalLibraryItemWithEpisode(id) } else { - val podcastEpisode = serverPodcastEpisodes.find { it.id == id } - return if (podcastEpisode != null && selectedLibraryItemWrapper != null) { - LibraryItemWithEpisode(selectedLibraryItemWrapper!!, podcastEpisode) - } else { - null - } + return podcastEpisodeLibraryItemMap[id] } } @@ -394,41 +351,6 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { } } - fun getMediaDescriptionFromMediaMetadata(item: MediaMetadataCompat, progress:MediaProgressWrapper?): MediaDescriptionCompat { - - val extras = Bundle() - if (progress != null) { - Log.d(tag, "Has media progress for ${item.description.title} | ${progress}") - if (progress.isFinished) { - extras.putInt( - MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, - MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED - ) - } else { - extras.putInt( - MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, - MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED - ) - extras.putDouble( - MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress - ) - } - } else { - Log.d(tag, "No media progress for ${item.description.title} | ${item.description.mediaId}") - extras.putInt( - MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, - MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED - ) - } - - return MediaDescriptionCompat.Builder() - .setMediaId(item.description.mediaId) - .setTitle(item.description.title) - .setIconUri(item.description.iconUri) - .setSubtitle(item.description.subtitle) - .setExtras(extras).build() - } - private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int { val lhsLength = lhs.length + 1 val rhsLength = rhs.length + 1 diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt b/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt index cf519f8e..099996c3 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt @@ -10,7 +10,7 @@ import com.audiobookshelf.app.data.* class BrowseTree( val context: Context, - libraryCategories: List, + itemsInProgress: List, libraries: List ) { private val mediaIdToChildren = mutableMapOf>() @@ -21,8 +21,7 @@ class BrowseTree( * @param drawableId - drawable res id * @return - uri */ - fun getUriToDrawable(context: Context, - @AnyRes drawableId: Int): Uri { + fun getUriToDrawable(@AnyRes drawableId: Int): Uri { return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.resources.getResourcePackageName(drawableId) + '/' + context.resources.getResourceTypeName(drawableId) @@ -32,38 +31,26 @@ class BrowseTree( init { val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf() - val continueReadingMetadata = MediaMetadataCompat.Builder().apply { + val continueListeningMetadata = MediaMetadataCompat.Builder().apply { putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT) putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Listening") - putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString()) + putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_localaudio).toString()) }.build() val downloadsMetadata = MediaMetadataCompat.Builder().apply { putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT) putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads") - putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString()) + putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_downloaddone).toString()) }.build() val librariesMetadata = MediaMetadataCompat.Builder().apply { putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT) putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries") - putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString()) + putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.icon_library_folder).toString()) }.build() - // Server continue Listening cat - libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory -> - val continueListeningMediaMetadata = continueListeningCategory.entities.map { liw -> - val libraryItem = liw as LibraryItem - libraryItem.getMediaMetadata() - } - if (continueListeningMediaMetadata.isNotEmpty()) { - rootList += continueReadingMetadata - } - continueListeningMediaMetadata.forEach { - val children = mediaIdToChildren[CONTINUE_ROOT] ?: mutableListOf() - children += it - mediaIdToChildren[CONTINUE_ROOT] = children - } + if (!itemsInProgress.isEmpty()) { + rootList += continueListeningMetadata } if (libraries.isNotEmpty()) { @@ -78,23 +65,6 @@ class BrowseTree( } rootList += downloadsMetadata - libraryCategories.find { it.id == "local-books" }?.let { localBooksCat -> - localBooksCat.entities.forEach { libc -> - val libraryItem = libc as LocalLibraryItem - val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf() - children += libraryItem.getMediaMetadata() - mediaIdToChildren[DOWNLOADS_ROOT] = children - } - } - - libraryCategories.find { it.id == "local-podcasts" }?.let { localPodcastsCat -> - localPodcastsCat.entities.forEach { libc -> - val libraryItem = libc as LocalLibraryItem - val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf() - children += libraryItem.getMediaMetadata() - mediaIdToChildren[DOWNLOADS_ROOT] = children - } - } mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 92a0cdde..0ad77416 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -795,8 +795,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { private val VALID_MEDIA_BROWSERS = mutableListOf("com.audiobookshelf.app", ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME) private val AUTO_MEDIA_ROOT = "/" - private val ALL_ROOT = "__ALL__" private val LIBRARIES_ROOT = "__LIBRARIES__" + private val DOWNLOADS_ROOT = "__DOWNLOADS__" + private val CONTINUE_ROOT = "__CONTINUE__" private lateinit var browseTree:BrowseTree @@ -839,8 +840,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun onLoadChildren(parentMediaId: String, result: Result>) { Log.d(tag, "ON LOAD CHILDREN $parentMediaId") - val flag = if (parentMediaId == AUTO_MEDIA_ROOT || parentMediaId == LIBRARIES_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE - result.detach() if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes @@ -852,53 +851,67 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems -> val children = libraryItems.map { libraryItem -> - val libraryItemMediaMetadata = libraryItem.getMediaMetadata() - if (libraryItem.mediaType == "podcast") { // Podcasts are browseable - MediaBrowserCompat.MediaItem(libraryItemMediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) + val mediaDescription = libraryItem.getMediaDescription(null) + MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) } else { - val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItemMediaMetadata.description.mediaId } - val description = mediaManager.getMediaDescriptionFromMediaMetadata(libraryItemMediaMetadata, progress) + val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id } + val description = libraryItem.getMediaDescription(progress) MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) } } result.sendResult(children as MutableList?) } - } else if (parentMediaId == "__DOWNLOADS__") { // Load downloads + } else if (parentMediaId == DOWNLOADS_ROOT) { // Load downloads val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book") val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast") val localBrowseItems:MutableList = mutableListOf() localBooks.forEach { localLibraryItem -> - val mediaMetadata = localLibraryItem.getMediaMetadata() - val progress = DeviceManager.dbManager.getLocalMediaProgress(mediaMetadata.description.mediaId ?: "") - val description = mediaManager.getMediaDescriptionFromMediaMetadata(mediaMetadata, progress) + val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id) + val description = localLibraryItem.getMediaDescription(progress) localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) } localPodcasts.forEach { localLibraryItem -> - val mediaMetadata = localLibraryItem.getMediaMetadata() - localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) + val mediaDescription = localLibraryItem.getMediaDescription(null) + localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) } result.sendResult(localBrowseItems) - } else { // Load categories - mediaManager.loadAndroidAutoItems { libraryCategories -> - browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries) + } else if (parentMediaId == CONTINUE_ROOT) { + val localBrowseItems:MutableList = mutableListOf() + mediaManager.serverItemsInProgress.forEach { itemInProgress -> + val progress: MediaProgressWrapper? + val mediaDescription:MediaDescriptionCompat + if (itemInProgress.episode != null) { + if (itemInProgress.isLocal) { + progress = DeviceManager.dbManager.getLocalMediaProgress("${itemInProgress.libraryItemWrapper.id}-${itemInProgress.episode.id}") + } else { + progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == itemInProgress.libraryItemWrapper.id && it.episodeId == itemInProgress.episode.id } + } + mediaDescription = itemInProgress.episode.getMediaDescription(itemInProgress.libraryItemWrapper,progress) + } else { + if (itemInProgress.isLocal) { + progress = DeviceManager.dbManager.getLocalMediaProgress(itemInProgress.libraryItemWrapper.id) + } else { + progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == itemInProgress.libraryItemWrapper.id } + } + mediaDescription = itemInProgress.libraryItemWrapper.getMediaDescription(progress) + } + localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) + } + result.sendResult(localBrowseItems) + } else { + mediaManager.loadAndroidAutoItems { + browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries) val children = browseTree[parentMediaId]?.map { item -> - Log.d(tag, "Loading Browser Media Item ${item.description.title} $flag") - - if (flag == MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) { - val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == item.description.mediaId } - val description = mediaManager.getMediaDescriptionFromMediaMetadata(item, progress) - MediaBrowserCompat.MediaItem(description, flag) - } else { - MediaBrowserCompat.MediaItem(item.description, flag) - } + Log.d(tag, "Loading Browser Media Item ${item.description.title}") + MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) } result.sendResult(children as MutableList?) } @@ -907,10 +920,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun onSearch(query: String, extras: Bundle?, result: Result>) { result.detach() - mediaManager.loadAndroidAutoItems() { libraryCategories -> - browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries) - val children = browseTree[ALL_ROOT]?.map { item -> - MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) + mediaManager.loadAndroidAutoItems { + browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries) + val children = browseTree[LIBRARIES_ROOT]?.map { item -> + MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) } result.sendResult(children as MutableList?) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index 3893b558..bbf3a0ab 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -189,21 +189,16 @@ class ApiHandler(var ctx:Context) { } } - fun getLibraryCategories(libraryId:String, cb: (List) -> Unit) { - getRequest("/api/libraries/$libraryId/personalized", null, null) { - val items = mutableListOf() - if (it.has("value")) { - val array = it.getJSONArray("value") + fun getAllItemsInProgress(cb: (List) -> Unit) { + getRequest("/api/me/items-in-progress", null, null) { + val items = mutableListOf() + if (it.has("libraryItems")) { + val array = it.getJSONArray("libraryItems") for (i in 0 until array.length()) { val jsobj = array.get(i) as JSONObject - val type = jsobj.get("type").toString() - // Only support for podcast and book in android auto - if (type == "podcast" || type == "book") { - jsobj.put("isLocal", false) - val item = jacksonMapper.readValue(jsobj.toString()) - items.add(item) - } + val itemInProgress = ItemInProgress.makeFromServerObject(jsobj) + items.add(itemInProgress) } } cb(items) From b5e33b1707c5cc8021505175ae4200ffd20e3250 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Wed, 24 Aug 2022 19:33:10 -0400 Subject: [PATCH 23/66] Fix thread-safety with transaction on local media progress --- ios/App/App/plugins/AbsDatabase.swift | 5 ++-- .../models/local/LocalMediaProgress.swift | 29 +++++++++++-------- ios/App/Shared/player/PlayerProgress.swift | 1 - 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/ios/App/App/plugins/AbsDatabase.swift b/ios/App/App/plugins/AbsDatabase.swift index a78619df..4a36a1d8 100644 --- a/ios/App/App/plugins/AbsDatabase.swift +++ b/ios/App/App/plugins/AbsDatabase.swift @@ -176,10 +176,10 @@ public class AbsDatabase: CAPPlugin { call.reject("Local media progress not found or created") return } - localMediaProgress.updateFromServerMediaProgress(serverMediaProgress) NSLog("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress") - Database.shared.saveLocalMediaProgress(localMediaProgress) + localMediaProgress.updateFromServerMediaProgress(serverMediaProgress) + call.resolve(try localMediaProgress.asDictionary()) } catch { call.reject("Failed to sync media progress") @@ -203,7 +203,6 @@ public class AbsDatabase: CAPPlugin { // Update finished status localMediaProgress.updateIsFinished(isFinished) - Database.shared.saveLocalMediaProgress(localMediaProgress) // Build API response let progressDictionary = try? localMediaProgress.asDictionary() diff --git a/ios/App/Shared/models/local/LocalMediaProgress.swift b/ios/App/Shared/models/local/LocalMediaProgress.swift index 001e07ff..c15c38a9 100644 --- a/ios/App/Shared/models/local/LocalMediaProgress.swift +++ b/ios/App/Shared/models/local/LocalMediaProgress.swift @@ -158,19 +158,24 @@ extension LocalMediaProgress { } static func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) -> LocalMediaProgress? { - if let localMediaProgressId = localMediaProgressId { - // Check if it existing in the database, if not, we need to create it - if let progress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) { - return progress + let realm = try! Realm() + return try! realm.write { () -> LocalMediaProgress? in + if let localMediaProgressId = localMediaProgressId { + // Check if it existing in the database, if not, we need to create it + if let progress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) { + return progress + } + } + + if let localLibraryItemId = localLibraryItemId { + guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil } + let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId) + let progress = LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode) + realm.add(progress) + return progress + } else { + return nil } - } - - if let localLibraryItemId = localLibraryItemId { - guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil } - let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId) - return LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode) - } else { - return nil } } } diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 231ad102..afe03c86 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -84,7 +84,6 @@ class PlayerProgress { } localMediaProgress.updateFromPlaybackSession(session) - Database.shared.saveLocalMediaProgress(localMediaProgress) NSLog("Local progress saved to the database") From 01678f2c91c63b790724b6dc103139d02806bee1 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Wed, 24 Aug 2022 19:57:39 -0400 Subject: [PATCH 24/66] Remove DispatchQueue as that did not fix Realm crashes --- ios/App/Shared/player/AudioPlayer.swift | 8 +- ios/App/Shared/player/PlayerProgress.swift | 144 ++++++++++----------- 2 files changed, 72 insertions(+), 80 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 1f19feb6..f02b9708 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -18,6 +18,8 @@ enum PlayMethod:Int { } class AudioPlayer: NSObject { + private let queue = DispatchQueue(label: "ABSAudioPlayerQueue") + // enums and @objc are not compatible @objc dynamic var status: Int @objc dynamic var rate: Float @@ -141,7 +143,7 @@ class AudioPlayer: NSObject { // Rate will be different depending on playback speed, aim for 2 observations/sec let seconds = 0.5 * (self.rate > 0 ? self.rate : 1.0) let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale) - self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: PlayerProgress.queue) { [weak self] time in + self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: queue) { [weak self] time in let sleepTimeStopAt = self?.sleepTimeStopAt Task { // Let the player update the current playback positions @@ -205,7 +207,7 @@ class AudioPlayer: NSObject { private func startPausedTimer() { guard self.pausedTimer == nil else { return } - PlayerProgress.queue.async { + self.queue.async { self.pausedTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in NSLog("PAUSE TIMER: Syncing from server") Task { await PlayerProgress.shared.syncFromServer() } @@ -398,7 +400,7 @@ class AudioPlayer: NSObject { var times = [NSValue]() times.append(NSValue(time: sleepTime)) - sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: PlayerProgress.queue) { [weak self] in + sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: queue) { [weak self] in NSLog("SLEEP TIMER: Pausing audio") self?.pause() self?.removeSleepTimer() diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index afe03c86..47498a2e 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -11,7 +11,6 @@ import RealmSwift class PlayerProgress { public static let shared = PlayerProgress() - public static let queue = DispatchQueue(label: "ABSPlayerProgressQueue") private static let TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 10.0 @@ -46,50 +45,46 @@ class PlayerProgress { // MARK: - SYNC LOGIC private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) -> PlaybackSession? { - PlayerProgress.queue.sync { - guard let session = PlayerHandler.getPlaybackSession() else { return nil } - guard !currentTime.isNaN else { return nil } // Prevent bad data on player stop + guard let session = PlayerHandler.getPlaybackSession() else { return nil } + guard !currentTime.isNaN else { return nil } // Prevent bad data on player stop + + session.update { + session.realm?.refresh() - session.update { - session.realm?.refresh() - - let nowInSeconds = Date().timeIntervalSince1970 - let nowInMilliseconds = nowInSeconds * 1000 - let lastUpdateInMilliseconds = session.updatedAt ?? nowInMilliseconds - let lastUpdateInSeconds = lastUpdateInMilliseconds / 1000 - let secondsSinceLastUpdate = nowInSeconds - lastUpdateInSeconds - - session.currentTime = currentTime - session.updatedAt = nowInMilliseconds - - if includesPlayProgress { - session.timeListening += secondsSinceLastUpdate - } + let nowInSeconds = Date().timeIntervalSince1970 + let nowInMilliseconds = nowInSeconds * 1000 + let lastUpdateInMilliseconds = session.updatedAt ?? nowInMilliseconds + let lastUpdateInSeconds = lastUpdateInMilliseconds / 1000 + let secondsSinceLastUpdate = nowInSeconds - lastUpdateInSeconds + + session.currentTime = currentTime + session.updatedAt = nowInMilliseconds + + if includesPlayProgress { + session.timeListening += secondsSinceLastUpdate } - - return session.freeze() } + + return session.freeze() } private func updateLocalMediaProgressFromLocalSession() { - PlayerProgress.queue.sync { - guard let session = PlayerHandler.getPlaybackSession() else { return } - guard session.isLocal else { return } - - let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId) - guard let localMediaProgress = localMediaProgress else { - // Local media progress should have been created - // If we're here, it means a library id is invalid - return - } - - localMediaProgress.updateFromPlaybackSession(session) - - NSLog("Local progress saved to the database") - - // Send the local progress back to front-end - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil) + guard let session = PlayerHandler.getPlaybackSession() else { return } + guard session.isLocal else { return } + + let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId) + guard let localMediaProgress = localMediaProgress else { + // Local media progress should have been created + // If we're here, it means a library id is invalid + return } + + localMediaProgress.updateFromPlaybackSession(session) + + NSLog("Local progress saved to the database") + + // Send the local progress back to front-end + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil) } private func updateAllServerSessionFromLocalSession() async { @@ -105,33 +100,32 @@ class PlayerProgress { } private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async { - PlayerProgress.queue.sync { - var safeToSync = true - guard var session = session.thaw() else { return } - - // We need to update and check the server time in a transaction for thread-safety - session.update { - session.realm?.refresh() - - let nowInMilliseconds = Date().timeIntervalSince1970 * 1000 - let lastUpdateInMilliseconds = session.serverUpdatedAt - - // If required, rate limit requests based on session last update - if rateLimitSync { - let timeSinceLastSync = nowInMilliseconds - lastUpdateInMilliseconds - let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000 - safeToSync = timeSinceLastSync > timeBetweenSessionSync - if !safeToSync { - return // This only exits the update block - } - } - - session.serverUpdatedAt = nowInMilliseconds - } - session = session.freeze() - guard safeToSync else { return } - } + var safeToSync = true + guard var session = session.thaw() else { return } + + // We need to update and check the server time in a transaction for thread-safety + session.update { + session.realm?.refresh() + + let nowInMilliseconds = Date().timeIntervalSince1970 * 1000 + let lastUpdateInMilliseconds = session.serverUpdatedAt + + // If required, rate limit requests based on session last update + if rateLimitSync { + let timeSinceLastSync = nowInMilliseconds - lastUpdateInMilliseconds + let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000 + safeToSync = timeSinceLastSync > timeBetweenSessionSync + if !safeToSync { + return // This only exits the update block + } + } + + session.serverUpdatedAt = nowInMilliseconds + } + session = session.freeze() + + guard safeToSync else { return } NSLog("Sending sessionId(\(session.id)) to server") var success = false @@ -145,10 +139,8 @@ class PlayerProgress { // Remove old sessions after they synced with the server if success && !session.isActiveSession { - PlayerProgress.queue.sync { - if let session = session.thaw() { - session.delete() - } + if let session = session.thaw() { + session.delete() } } } @@ -183,16 +175,14 @@ class PlayerProgress { // Update the session, if needed if serverIsNewerThanLocal && currentTimeIsDifferent { - PlayerProgress.queue.sync { - NSLog("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)") - guard let session = session.thaw() else { return } - session.update { - session.currentTime = serverCurrentTime - session.updatedAt = serverLastUpdate - } - NSLog("updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)") - PlayerHandler.seek(amount: session.currentTime) + NSLog("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)") + guard let session = session.thaw() else { return } + session.update { + session.currentTime = serverCurrentTime + session.updatedAt = serverLastUpdate } + NSLog("updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)") + PlayerHandler.seek(amount: session.currentTime) } else { NSLog("updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress") } From 8c87b31e56c0dfef7df90b4166351fa5ece29016 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 25 Aug 2022 15:42:37 -0400 Subject: [PATCH 25/66] Improved error handling --- ios/App/App/plugins/AbsAudioPlayer.swift | 16 ++- ios/App/App/plugins/AbsDatabase.swift | 54 +++++---- ios/App/App/plugins/AbsDownloader.swift | 10 +- ios/App/App/plugins/AbsFileSystem.swift | 35 +++--- .../Shared/models/download/DownloadItem.swift | 4 +- .../models/local/LocalLibraryItem.swift | 4 +- .../models/local/LocalMediaProgress.swift | 18 +-- ios/App/Shared/models/server/AudioTrack.swift | 2 +- ios/App/Shared/player/AudioPlayer.swift | 2 +- ios/App/Shared/player/PlayerHandler.swift | 21 ++-- ios/App/Shared/player/PlayerProgress.swift | 59 +++++---- ios/App/Shared/util/ApiClient.swift | 7 +- ios/App/Shared/util/DaoExtensions.swift | 16 +-- ios/App/Shared/util/Database.swift | 113 ++++++++++++------ 14 files changed, 220 insertions(+), 141 deletions(-) diff --git a/ios/App/App/plugins/AbsAudioPlayer.swift b/ios/App/App/plugins/AbsAudioPlayer.swift index 2b4197fc..44b3b374 100644 --- a/ios/App/App/plugins/AbsAudioPlayer.swift +++ b/ios/App/App/plugins/AbsAudioPlayer.swift @@ -81,9 +81,9 @@ public class AbsAudioPlayer: CAPPlugin { NSLog("Failed to get local playback session") return call.resolve([:]) } - playbackSession.save() do { + try playbackSession.save() try self.startPlaybackSession(playbackSession, playWhenReady: playWhenReady, playbackRate: playbackRate) call.resolve(try playbackSession.asDictionary()) } catch(let exception) { @@ -93,8 +93,8 @@ public class AbsAudioPlayer: CAPPlugin { } } else { // Playing from the server ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId, forceTranscode: false) { session in - session.save() do { + try session.save() try self.startPlaybackSession(session, playWhenReady: playWhenReady, playbackRate: playbackRate) call.resolve(try session.asDictionary()) } catch(let exception) { @@ -122,7 +122,7 @@ public class AbsAudioPlayer: CAPPlugin { @objc func setPlaybackSpeed(_ call: CAPPluginCall) { let playbackRate = call.getFloat("value", 1.0) let settings = PlayerSettings.main() - settings.update { + try? settings.update { settings.playbackRate = playbackRate } PlayerHandler.setPlaybackSpeed(speed: settings.playbackRate) @@ -244,17 +244,15 @@ public class AbsAudioPlayer: CAPPlugin { // If direct playing then fallback to transcode ApiClient.startPlaybackSession(libraryItemId: libraryItemId, episodeId: episodeId, forceTranscode: true) { session in - session.save() - PlayerHandler.startPlayback(sessionId: session.id, playWhenReady: self.initialPlayWhenReady, playbackRate: PlayerSettings.main().playbackRate) - do { + try session.save() + PlayerHandler.startPlayback(sessionId: session.id, playWhenReady: self.initialPlayWhenReady, playbackRate: PlayerSettings.main().playbackRate) self.sendPlaybackSession(session: try session.asDictionary()) + self.sendMetadata() } catch(let exception) { - NSLog("failed to convert session to json") + NSLog("Failed to start transcoded session") debugPrint(exception) } - - self.sendMetadata() } } else { self.notifyListeners("onPlaybackFailed", data: [ diff --git a/ios/App/App/plugins/AbsDatabase.swift b/ios/App/App/plugins/AbsDatabase.swift index 4a36a1d8..3a281277 100644 --- a/ios/App/App/plugins/AbsDatabase.swift +++ b/ios/App/App/plugins/AbsDatabase.swift @@ -139,7 +139,7 @@ public class AbsDatabase: CAPPlugin { call.reject("localMediaProgressId not specificed") return } - Database.shared.removeLocalMediaProgress(localMediaProgressId: localMediaProgressId) + try? Database.shared.removeLocalMediaProgress(localMediaProgressId: localMediaProgressId) call.resolve() } @@ -171,14 +171,14 @@ public class AbsDatabase: CAPPlugin { return call.reject("localLibraryItemId or localMediaProgressId must be specified") } - let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) + let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) guard let localMediaProgress = localMediaProgress else { call.reject("Local media progress not found or created") return } NSLog("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress") - localMediaProgress.updateFromServerMediaProgress(serverMediaProgress) + try localMediaProgress.updateFromServerMediaProgress(serverMediaProgress) call.resolve(try localMediaProgress.asDictionary()) } catch { @@ -195,30 +195,36 @@ public class AbsDatabase: CAPPlugin { NSLog("updateLocalMediaProgressFinished \(localMediaProgressId ?? "Unknown") | Is Finished: \(isFinished)") - let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) - guard let localMediaProgress = localMediaProgress else { - call.resolve(["error": "Library Item not found"]) - return - } + do { + let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) + guard let localMediaProgress = localMediaProgress else { + call.resolve(["error": "Library Item not found"]) + return + } - // Update finished status - localMediaProgress.updateIsFinished(isFinished) - - // Build API response - let progressDictionary = try? localMediaProgress.asDictionary() - var response: [String: Any] = ["local": true, "server": false, "localMediaProgress": progressDictionary ?? ""] - - // Send update to the server if logged in - let hasLinkedServer = localMediaProgress.serverConnectionConfigId != nil - let loggedIntoServer = Store.serverConfig?.id == localMediaProgress.serverConnectionConfigId - if hasLinkedServer && loggedIntoServer { - response["server"] = true - let payload = ["isFinished": isFinished] - ApiClient.updateMediaProgress(libraryItemId: localMediaProgress.libraryItemId!, episodeId: localEpisodeId, payload: payload) { + // Update finished status + try localMediaProgress.updateIsFinished(isFinished) + + // Build API response + let progressDictionary = try? localMediaProgress.asDictionary() + var response: [String: Any] = ["local": true, "server": false, "localMediaProgress": progressDictionary ?? ""] + + // Send update to the server if logged in + let hasLinkedServer = localMediaProgress.serverConnectionConfigId != nil + let loggedIntoServer = Store.serverConfig?.id == localMediaProgress.serverConnectionConfigId + if hasLinkedServer && loggedIntoServer { + response["server"] = true + let payload = ["isFinished": isFinished] + ApiClient.updateMediaProgress(libraryItemId: localMediaProgress.libraryItemId!, episodeId: localEpisodeId, payload: payload) { + call.resolve(response) + } + } else { call.resolve(response) } - } else { - call.resolve(response) + } catch { + debugPrint(error) + call.resolve(["error": "Failed to mark as complete"]) + return } } diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index a5dbffa2..d430fd5e 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -28,7 +28,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in - let realm = try! Realm() + let realm = try Realm() try realm.write { downloadItemPart.progress = 100 downloadItemPart.completed = true @@ -139,7 +139,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { } self.handleDownloadTaskCompleteFromDownloadItem(item) if let item = Database.shared.getDownloadItem(downloadItemId: item.id!) { - item.delete() + try? item.delete() } } @@ -181,7 +181,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { } } else { localLibraryItem = LocalLibraryItem(libraryItem, localUrl: localDirectory, server: Store.serverConfig!, files: files, coverPath: coverFile) - Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem!) + try? Database.shared.saveLocalLibraryItem(localLibraryItem: localLibraryItem!) } statusNotification["localLibraryItem"] = try? localLibraryItem.asDictionary() @@ -189,7 +189,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { if let progress = libraryItem.userMediaProgress { let episode = downloadItem.media?.episodes.first(where: { $0.id == downloadItem.episodeId }) let localMediaProgress = LocalMediaProgress(localLibraryItem: localLibraryItem!, episode: episode, progress: progress) - Database.shared.saveLocalMediaProgress(localMediaProgress) + try? localMediaProgress.save() statusNotification["localMediaProgress"] = try? localMediaProgress.asDictionary() } @@ -276,7 +276,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { } // Persist in the database before status start coming in - Database.shared.saveDownloadItem(downloadItem) + try Database.shared.saveDownloadItem(downloadItem) // Start all the downloads for task in tasks { diff --git a/ios/App/App/plugins/AbsFileSystem.swift b/ios/App/App/plugins/AbsFileSystem.swift index 928eed64..a96e53c6 100644 --- a/ios/App/App/plugins/AbsFileSystem.swift +++ b/ios/App/App/plugins/AbsFileSystem.swift @@ -70,7 +70,7 @@ public class AbsFileSystem: CAPPlugin { do { if let localLibraryItemId = localLibraryItemId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) { try FileManager.default.removeItem(at: item.contentDirectory!) - item.delete() + try item.delete() success = true } } catch { @@ -89,24 +89,29 @@ public class AbsFileSystem: CAPPlugin { var success = false if let localLibraryItemId = localLibraryItemId, let trackLocalFileId = trackLocalFileId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) { - item.update { - do { - if let fileIndex = item.localFiles.firstIndex(where: { $0.id == trackLocalFileId }) { - try FileManager.default.removeItem(at: item.localFiles[fileIndex].contentPath) - item.realm?.delete(item.localFiles[fileIndex]) - if item.isPodcast, let media = item.media { - if let episodeIndex = media.episodes.firstIndex(where: { $0.audioTrack?.localFileId == trackLocalFileId }) { - media.episodes.remove(at: episodeIndex) + do { + try item.update { + do { + if let fileIndex = item.localFiles.firstIndex(where: { $0.id == trackLocalFileId }) { + try FileManager.default.removeItem(at: item.localFiles[fileIndex].contentPath) + item.realm?.delete(item.localFiles[fileIndex]) + if item.isPodcast, let media = item.media { + if let episodeIndex = media.episodes.firstIndex(where: { $0.audioTrack?.localFileId == trackLocalFileId }) { + media.episodes.remove(at: episodeIndex) + } + item.media = media } - item.media = media + call.resolve(try item.asDictionary()) + success = true } - call.resolve(try item.asDictionary()) - success = true + } catch { + NSLog("Failed to delete \(error)") + success = false } - } catch { - NSLog("Failed to delete \(error)") - success = false } + } catch { + NSLog("Failed to delete \(error)") + success = false } } diff --git a/ios/App/Shared/models/download/DownloadItem.swift b/ios/App/Shared/models/download/DownloadItem.swift index 446cadcc..ee30213a 100644 --- a/ios/App/Shared/models/download/DownloadItem.swift +++ b/ios/App/Shared/models/download/DownloadItem.swift @@ -88,8 +88,8 @@ extension DownloadItem { self.downloadItemParts.allSatisfy({ $0.failed == false }) } - func delete() { - try! self.realm?.write { + func delete() throws { + try self.realm?.write { self.realm?.delete(self.downloadItemParts) self.realm?.delete(self) } diff --git a/ios/App/Shared/models/local/LocalLibraryItem.swift b/ios/App/Shared/models/local/LocalLibraryItem.swift index d2ef503c..51d6a8bb 100644 --- a/ios/App/Shared/models/local/LocalLibraryItem.swift +++ b/ios/App/Shared/models/local/LocalLibraryItem.swift @@ -198,8 +198,8 @@ extension LocalLibraryItem { ) } - func delete() { - try! self.realm?.write { + func delete() throws { + try self.realm?.write { self.realm?.delete(self.localFiles) self.realm?.delete(self) } diff --git a/ios/App/Shared/models/local/LocalMediaProgress.swift b/ios/App/Shared/models/local/LocalMediaProgress.swift index c15c38a9..c034ae8d 100644 --- a/ios/App/Shared/models/local/LocalMediaProgress.swift +++ b/ios/App/Shared/models/local/LocalMediaProgress.swift @@ -119,8 +119,8 @@ extension LocalMediaProgress { self.finishedAt = progress.finishedAt } - func updateIsFinished(_ finished: Bool) { - try! self.realm?.write { + func updateIsFinished(_ finished: Bool) throws { + try self.realm?.write { if self.isFinished != finished { self.progress = finished ? 1.0 : 0.0 } @@ -135,8 +135,8 @@ extension LocalMediaProgress { } } - func updateFromPlaybackSession(_ playbackSession: PlaybackSession) { - try! self.realm?.write { + func updateFromPlaybackSession(_ playbackSession: PlaybackSession) throws { + try self.realm?.write { self.currentTime = playbackSession.currentTime self.progress = playbackSession.progress self.lastUpdate = Date().timeIntervalSince1970 * 1000 @@ -145,8 +145,8 @@ extension LocalMediaProgress { } } - func updateFromServerMediaProgress(_ serverMediaProgress: MediaProgress) { - try! self.realm?.write { + func updateFromServerMediaProgress(_ serverMediaProgress: MediaProgress) throws { + try self.realm?.write { self.isFinished = serverMediaProgress.isFinished self.progress = serverMediaProgress.progress self.currentTime = serverMediaProgress.currentTime @@ -157,9 +157,9 @@ extension LocalMediaProgress { } } - static func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) -> LocalMediaProgress? { - let realm = try! Realm() - return try! realm.write { () -> LocalMediaProgress? in + static func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) throws -> LocalMediaProgress? { + let realm = try Realm() + return try realm.write { () -> LocalMediaProgress? in if let localMediaProgressId = localMediaProgressId { // Check if it existing in the database, if not, we need to create it if let progress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) { diff --git a/ios/App/Shared/models/server/AudioTrack.swift b/ios/App/Shared/models/server/AudioTrack.swift index c0d0ab88..8cd9e50e 100644 --- a/ios/App/Shared/models/server/AudioTrack.swift +++ b/ios/App/Shared/models/server/AudioTrack.swift @@ -37,7 +37,7 @@ class AudioTrack: EmbeddedObject, Codable { contentUrl = try? values.decode(String.self, forKey: .contentUrl) mimeType = try values.decode(String.self, forKey: .mimeType) metadata = try? values.decode(FileMetadata.self, forKey: .metadata) - localFileId = try! values.decodeIfPresent(String.self, forKey: .localFileId) + localFileId = try? values.decodeIfPresent(String.self, forKey: .localFileId) serverIndex = try? values.decode(Int.self, forKey: .serverIndex) } diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index f02b9708..70e334ec 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -306,7 +306,7 @@ class AudioPlayer: NSObject { if (self.currentTrackIndex != indexOfSeek) { self.currentTrackIndex = indexOfSeek - playbackSession.update { + try? playbackSession.update { playbackSession.currentTime = to } diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index 0a330f61..505c58b8 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -158,16 +158,21 @@ class PlayerHandler { // MARK: - Helper logic private static func cleanupOldSessions(currentSessionId: String?) { - let realm = try! Realm() - let oldSessions = realm.objects(PlaybackSession.self) .where({ - $0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id - }) - try! realm.write { - for s in oldSessions { - if s.id != currentSessionId { - s.isActiveSession = false + do { + let realm = try Realm() + let oldSessions = realm.objects(PlaybackSession.self) .where({ + $0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id + }) + try realm.write { + for s in oldSessions { + if s.id != currentSessionId { + s.isActiveSession = false + } } } + } catch { + debugPrint("Failed to cleanup sessions") + debugPrint(error) } } } diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index 47498a2e..d99a15a4 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -21,34 +21,49 @@ class PlayerProgress { public func syncFromPlayer(currentTime: Double, includesPlayProgress: Bool, isStopping: Bool) async { let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromPlayer") - let session = updateLocalSessionFromPlayer(currentTime: currentTime, includesPlayProgress: includesPlayProgress) - updateLocalMediaProgressFromLocalSession() - if let session = session { - await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping) + do { + let session = try updateLocalSessionFromPlayer(currentTime: currentTime, includesPlayProgress: includesPlayProgress) + try updateLocalMediaProgressFromLocalSession() + if let session = session { + try await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping) + } + } catch { + debugPrint("Failed to syncFromPlayer") + debugPrint(error) } await UIApplication.shared.endBackgroundTask(backgroundToken) } public func syncToServer() async { let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncToServer") - await updateAllServerSessionFromLocalSession() + do { + try await updateAllServerSessionFromLocalSession() + } catch { + debugPrint("Failed to syncToServer") + debugPrint(error) + } await UIApplication.shared.endBackgroundTask(backgroundToken) } public func syncFromServer() async { let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromServer") - await updateLocalSessionFromServerMediaProgress() + do { + try await updateLocalSessionFromServerMediaProgress() + } catch { + debugPrint("Failed to syncFromServer") + debugPrint(error) + } await UIApplication.shared.endBackgroundTask(backgroundToken) } // MARK: - SYNC LOGIC - private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) -> PlaybackSession? { + private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) throws -> PlaybackSession? { guard let session = PlayerHandler.getPlaybackSession() else { return nil } guard !currentTime.isNaN else { return nil } // Prevent bad data on player stop - session.update { + try session.update { session.realm?.refresh() let nowInSeconds = Date().timeIntervalSince1970 @@ -68,18 +83,18 @@ class PlayerProgress { return session.freeze() } - private func updateLocalMediaProgressFromLocalSession() { + private func updateLocalMediaProgressFromLocalSession() throws { guard let session = PlayerHandler.getPlaybackSession() else { return } guard session.isLocal else { return } - let localMediaProgress = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId) + let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId) guard let localMediaProgress = localMediaProgress else { // Local media progress should have been created // If we're here, it means a library id is invalid return } - localMediaProgress.updateFromPlaybackSession(session) + try localMediaProgress.updateFromPlaybackSession(session) NSLog("Local progress saved to the database") @@ -87,25 +102,25 @@ class PlayerProgress { NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil) } - private func updateAllServerSessionFromLocalSession() async { - await withTaskGroup(of: Void.self) { [self] group in - for session in try! await Realm().objects(PlaybackSession.self).where({ $0.serverConnectionConfigId == Store.serverConfig?.id }) { + private func updateAllServerSessionFromLocalSession() async throws { + try await withThrowingTaskGroup(of: Void.self) { [self] group in + for session in try await Realm().objects(PlaybackSession.self).where({ $0.serverConnectionConfigId == Store.serverConfig?.id }) { let session = session.freeze() group.addTask { - await self.updateServerSessionFromLocalSession(session) + try await self.updateServerSessionFromLocalSession(session) } } - await group.waitForAll() + try await group.waitForAll() } } - private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async { + private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async throws { var safeToSync = true guard var session = session.thaw() else { return } // We need to update and check the server time in a transaction for thread-safety - session.update { + try session.update { session.realm?.refresh() let nowInMilliseconds = Date().timeIntervalSince1970 * 1000 @@ -140,14 +155,14 @@ class PlayerProgress { // Remove old sessions after they synced with the server if success && !session.isActiveSession { if let session = session.thaw() { - session.delete() + try session.delete() } } } - private func updateLocalSessionFromServerMediaProgress() async { + private func updateLocalSessionFromServerMediaProgress() async throws { NSLog("updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server") - guard let session = try! await Realm().objects(PlaybackSession.self).last(where: { + guard let session = try await Realm().objects(PlaybackSession.self).last(where: { $0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id })?.freeze() else { NSLog("updateLocalSessionFromServerMediaProgress: Failed to get session") @@ -177,7 +192,7 @@ class PlayerProgress { if serverIsNewerThanLocal && currentTimeIsDifferent { NSLog("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)") guard let session = session.thaw() else { return } - session.update { + try session.update { session.currentTime = serverCurrentTime session.updatedAt = serverLastUpdate } diff --git a/ios/App/Shared/util/ApiClient.swift b/ios/App/Shared/util/ApiClient.swift index 2d5258b0..2c247e35 100644 --- a/ios/App/Shared/util/ApiClient.swift +++ b/ios/App/Shared/util/ApiClient.swift @@ -200,7 +200,12 @@ class ApiClient { if let updates = response.localProgressUpdates { for update in updates { - Database.shared.saveLocalMediaProgress(update) + do { + try update.save() + } catch { + debugPrint("Failed to update local media progress") + debugPrint(error) + } } } diff --git a/ios/App/Shared/util/DaoExtensions.swift b/ios/App/Shared/util/DaoExtensions.swift index f1d61c3a..67c7923a 100644 --- a/ios/App/Shared/util/DaoExtensions.swift +++ b/ios/App/Shared/util/DaoExtensions.swift @@ -9,15 +9,15 @@ import Foundation import RealmSwift extension Object { - func save() { - let realm = try! Realm() - try! realm.write { + func save() throws { + let realm = try Realm() + try realm.write { realm.add(self, update: .modified) } } - func update(handler: () -> Void) { - try! self.realm?.write { + func update(handler: () -> Void) throws { + try self.realm?.write { handler() } } @@ -33,12 +33,12 @@ extension EmbeddedObject { } protocol Deletable { - func delete() + func delete() throws } extension Deletable where Self: Object { - func delete() { - try! self.realm?.write { + func delete() throws { + try self.realm?.write { self.realm?.delete(self) } } diff --git a/ios/App/Shared/util/Database.swift b/ios/App/Shared/util/Database.swift index 396682ae..721f7121 100644 --- a/ios/App/Shared/util/Database.swift +++ b/ios/App/Shared/util/Database.swift @@ -112,48 +112,83 @@ class Database { } public func getLocalLibraryItems(mediaType: MediaType? = nil) -> [LocalLibraryItem] { - let realm = try! Realm() - return Array(realm.objects(LocalLibraryItem.self)) + do { + let realm = try Realm() + return Array(realm.objects(LocalLibraryItem.self)) + } catch { + debugPrint(error) + return [] + } } public func getLocalLibraryItem(byServerLibraryItemId: String) -> LocalLibraryItem? { - let realm = try! Realm() - return realm.objects(LocalLibraryItem.self).first(where: { $0.libraryItemId == byServerLibraryItemId }) + do { + let realm = try Realm() + return realm.objects(LocalLibraryItem.self).first(where: { $0.libraryItemId == byServerLibraryItemId }) + } catch { + debugPrint(error) + return nil + } } public func getLocalLibraryItem(localLibraryItemId: String) -> LocalLibraryItem? { - let realm = try! Realm() - return realm.object(ofType: LocalLibraryItem.self, forPrimaryKey: localLibraryItemId) + do { + let realm = try Realm() + return realm.object(ofType: LocalLibraryItem.self, forPrimaryKey: localLibraryItemId) + } catch { + debugPrint(error) + return nil + } } - public func saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) { - let realm = try! Realm() - try! realm.write { realm.add(localLibraryItem, update: .modified) } + public func saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) throws { + let realm = try Realm() + try realm.write { realm.add(localLibraryItem, update: .modified) } } public func getLocalFile(localFileId: String) -> LocalFile? { - let realm = try! Realm() - return realm.object(ofType: LocalFile.self, forPrimaryKey: localFileId) + do { + let realm = try Realm() + return realm.object(ofType: LocalFile.self, forPrimaryKey: localFileId) + } catch { + debugPrint(error) + return nil + } } public func getDownloadItem(downloadItemId: String) -> DownloadItem? { - let realm = try! Realm() - return realm.object(ofType: DownloadItem.self, forPrimaryKey: downloadItemId) + do { + let realm = try Realm() + return realm.object(ofType: DownloadItem.self, forPrimaryKey: downloadItemId) + } catch { + debugPrint(error) + return nil + } } public func getDownloadItem(libraryItemId: String) -> DownloadItem? { - let realm = try! Realm() - return realm.objects(DownloadItem.self).filter("libraryItemId == %@", libraryItemId).first + do { + let realm = try Realm() + return realm.objects(DownloadItem.self).filter("libraryItemId == %@", libraryItemId).first + } catch { + debugPrint(error) + return nil + } } public func getDownloadItem(downloadItemPartId: String) -> DownloadItem? { - let realm = try! Realm() - return realm.objects(DownloadItem.self).filter("SUBQUERY(downloadItemParts, $part, $part.id == %@) .@count > 0", downloadItemPartId).first + do { + let realm = try Realm() + return realm.objects(DownloadItem.self).filter("SUBQUERY(downloadItemParts, $part, $part.id == %@) .@count > 0", downloadItemPartId).first + } catch { + debugPrint(error) + return nil + } } - public func saveDownloadItem(_ downloadItem: DownloadItem) { - let realm = try! Realm() - return try! realm.write { realm.add(downloadItem, update: .modified) } + public func saveDownloadItem(_ downloadItem: DownloadItem) throws { + let realm = try Realm() + return try realm.write { realm.add(downloadItem, update: .modified) } } public func getDeviceSettings() -> DeviceSettings { @@ -162,31 +197,41 @@ class Database { } public func getAllLocalMediaProgress() -> [LocalMediaProgress] { - let realm = try! Realm() - return Array(realm.objects(LocalMediaProgress.self)) - } - - public func saveLocalMediaProgress(_ mediaProgress: LocalMediaProgress) { - let realm = try! Realm() - try! realm.write { realm.add(mediaProgress, update: .modified) } + do { + let realm = try Realm() + return Array(realm.objects(LocalMediaProgress.self)) + } catch { + debugPrint(error) + return [] + } } // For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}" public func getLocalMediaProgress(localMediaProgressId: String) -> LocalMediaProgress? { - let realm = try! Realm() - return realm.object(ofType: LocalMediaProgress.self, forPrimaryKey: localMediaProgressId) + do { + let realm = try Realm() + return realm.object(ofType: LocalMediaProgress.self, forPrimaryKey: localMediaProgressId) + } catch { + debugPrint(error) + return nil + } } - public func removeLocalMediaProgress(localMediaProgressId: String) { - let realm = try! Realm() - try! realm.write { + public func removeLocalMediaProgress(localMediaProgressId: String) throws { + let realm = try Realm() + try realm.write { let progress = realm.object(ofType: LocalMediaProgress.self, forPrimaryKey: localMediaProgressId) realm.delete(progress!) } } public func getPlaybackSession(id: String) -> PlaybackSession? { - let realm = try! Realm() - return realm.object(ofType: PlaybackSession.self, forPrimaryKey: id) + do { + let realm = try Realm() + return realm.object(ofType: PlaybackSession.self, forPrimaryKey: id) + } catch { + debugPrint(error) + return nil + } } } From 5c76158729cf2a1e00cc3d17a4f15bcd400bdd73 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 25 Aug 2022 16:55:35 -0400 Subject: [PATCH 26/66] Fix holding onto frozen Realm reference --- ios/App/Shared/util/Store.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ios/App/Shared/util/Store.swift b/ios/App/Shared/util/Store.swift index 56fe7ad1..6073f2d7 100644 --- a/ios/App/Shared/util/Store.swift +++ b/ios/App/Shared/util/Store.swift @@ -9,10 +9,17 @@ import Foundation import RealmSwift class Store { - private static var _serverConfig: ServerConnectionConfig? public static var serverConfig: ServerConnectionConfig? { get { - return _serverConfig + do { + // Fetch each time, as holding onto a live or frozen realm object is bad + let index = Database.shared.getLastActiveConfigIndex() + let realm = try Realm() + return realm.objects(ServerConnectionConfig.self).first(where: { $0.index == index }) + } catch { + debugPrint(error) + return nil + } } set(updated) { if updated != nil { @@ -20,9 +27,6 @@ class Store { } else { Database.shared.setLastActiveConfigIndexToNil() } - - // Make safe for accessing on all threads - _serverConfig = updated?.freeze() } } } From 268cf6757625031c4bd6c4d2d6d39b9c6f2b9a61 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 25 Aug 2022 17:39:06 -0400 Subject: [PATCH 27/66] Fix lost sleep time on play/pause --- ios/App/Shared/player/AudioPlayer.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 70e334ec..ac2585ed 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -224,6 +224,9 @@ class AudioPlayer: NSObject { public func play(allowSeekBack: Bool = false) { guard self.isInitialized() else { return } + // Capture remaining sleep time before changing the track position + let sleepSecondsRemaining = PlayerHandler.remainingSleepTime + if allowSeekBack { let diffrence = Date.timeIntervalSinceReferenceDate - lastPlayTime var time: Int? @@ -262,6 +265,9 @@ class AudioPlayer: NSObject { self.rate = self.tmpRate self.audioPlayer.rate = self.tmpRate + // If we have an active sleep timer, reschedule based on rate + self.rescheduleSleepTimerAtTime(time: self.getCurrentTime(), secondsRemaining: sleepSecondsRemaining) + updateNowPlaying() } From 7cf36d829a75cc183626789ceed4b643ee5527c3 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 25 Aug 2022 18:28:17 -0400 Subject: [PATCH 28/66] Fix progress updating issues --- ios/App/Shared/player/AudioPlayer.swift | 35 ++++++++++++------------ ios/App/Shared/util/NowPlayingInfo.swift | 17 ++++++++---- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index ac2585ed..b002b3ad 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -144,15 +144,14 @@ class AudioPlayer: NSObject { let seconds = 0.5 * (self.rate > 0 ? self.rate : 1.0) let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale) self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: queue) { [weak self] time in - let sleepTimeStopAt = self?.sleepTimeStopAt Task { // Let the player update the current playback positions await PlayerProgress.shared.syncFromPlayer(currentTime: time.seconds, includesPlayProgress: true, isStopping: false) - - // Update the sleep time, if set - if sleepTimeStopAt != nil { - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) - } + } + + // Update the sleep time, if set + if self?.sleepTimeStopAt != nil { + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) } } } @@ -351,25 +350,27 @@ class AudioPlayer: NSObject { } public func setPlaybackRate(_ rate: Float, observed: Bool = false) { + // Capture remaining sleep time before changing the rate + let sleepSecondsRemaining = PlayerHandler.remainingSleepTime + let playbackSpeedChanged = rate > 0.0 && rate != self.tmpRate && !(observed && rate == 1) + if self.audioPlayer.rate != rate { NSLog("setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)") self.audioPlayer.rate = rate } - if rate > 0.0 && !(observed && rate == 1) { - self.tmpRate = rate - } - - // Capture remaining sleep time before changing the rate - let sleepSecondsRemaining = PlayerHandler.remainingSleepTime self.rate = rate self.updateNowPlaying() - // If we have an active sleep timer, reschedule based on rate - self.rescheduleSleepTimerAtTime(time: self.getCurrentTime(), secondsRemaining: sleepSecondsRemaining) - - // Setup the time observer again at the new rate - self.setupTimeObserver() + if playbackSpeedChanged { + self.tmpRate = rate + + // If we have an active sleep timer, reschedule based on rate + self.rescheduleSleepTimerAtTime(time: self.getCurrentTime(), secondsRemaining: sleepSecondsRemaining) + + // Setup the time observer again at the new rate + self.setupTimeObserver() + } } public func getSleepStopAt() -> Double? { diff --git a/ios/App/Shared/util/NowPlayingInfo.swift b/ios/App/Shared/util/NowPlayingInfo.swift index 01e6d386..387c8911 100644 --- a/ios/App/Shared/util/NowPlayingInfo.swift +++ b/ios/App/Shared/util/NowPlayingInfo.swift @@ -59,12 +59,17 @@ class NowPlayingInfo { } } public func update(duration: Double, currentTime: Double, rate: Float) { - nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration - nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime - nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate - nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0 - - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + // Update on the main to prevent access collisions + DispatchQueue.main.async { [weak self] in + if let self = self { + self.nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration + self.nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime + self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate + self.nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0 + + MPNowPlayingInfoCenter.default().nowPlayingInfo = self.nowPlayingInfo + } + } } public func reset() { From 3e31e727343fd52820994d5d9a7fe18427dd0848 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 25 Aug 2022 18:31:09 -0400 Subject: [PATCH 29/66] Configure time observer on the main queue --- ios/App/Shared/player/AudioPlayer.swift | 31 ++++++++++++++----------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index b002b3ad..77f4dbce 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -137,21 +137,24 @@ class AudioPlayer: NSObject { } private func setupTimeObserver() { - removeTimeObserver() - - let timeScale = CMTimeScale(NSEC_PER_SEC) - // Rate will be different depending on playback speed, aim for 2 observations/sec - let seconds = 0.5 * (self.rate > 0 ? self.rate : 1.0) - let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale) - self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: queue) { [weak self] time in - Task { - // Let the player update the current playback positions - await PlayerProgress.shared.syncFromPlayer(currentTime: time.seconds, includesPlayProgress: true, isStopping: false) - } + // Time observer should be configured on the main queue + DispatchQueue.main.sync { + self.removeTimeObserver() - // Update the sleep time, if set - if self?.sleepTimeStopAt != nil { - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) + let timeScale = CMTimeScale(NSEC_PER_SEC) + // Rate will be different depending on playback speed, aim for 2 observations/sec + let seconds = 0.5 * (self.rate > 0 ? self.rate : 1.0) + let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale) + self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: queue) { [weak self] time in + Task { + // Let the player update the current playback positions + await PlayerProgress.shared.syncFromPlayer(currentTime: time.seconds, includesPlayProgress: true, isStopping: false) + } + + // Update the sleep time, if set + if self?.sleepTimeStopAt != nil { + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) + } } } } From aed2c31f5a2e4a1de23c6ff4cdfd28523b37ca9a Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 25 Aug 2022 18:43:57 -0400 Subject: [PATCH 30/66] Use persisted session to inform seek back --- ios/App/Shared/player/AudioPlayer.swift | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 77f4dbce..7d743054 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -25,7 +25,6 @@ class AudioPlayer: NSObject { @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 @@ -229,21 +228,22 @@ class AudioPlayer: NSObject { // Capture remaining sleep time before changing the track position let sleepSecondsRemaining = PlayerHandler.remainingSleepTime - if allowSeekBack { - let diffrence = Date.timeIntervalSinceReferenceDate - lastPlayTime + if allowSeekBack, let session = Database.shared.getPlaybackSession(id: self.sessionId) { + let lastPlayed = (session.updatedAt ?? 0)/1000 + let difference = Date.timeIntervalSinceReferenceDate - lastPlayed var time: Int? - if lastPlayTime == 0 { + if lastPlayed == 0 { time = 5 - } else if diffrence < 6 { + } else if difference < 6 { time = 2 - } else if diffrence < 12 { + } else if difference < 12 { time = 10 - } else if diffrence < 30 { + } else if difference < 30 { time = 15 - } else if diffrence < 180 { + } else if difference < 180 { time = 20 - } else if diffrence < 3600 { + } else if difference < 3600 { time = 25 } else { time = 29 @@ -253,7 +253,6 @@ class AudioPlayer: NSObject { seek(getCurrentTime() - Double(time!), from: "play") } } - lastPlayTime = Date.timeIntervalSinceReferenceDate self.stopPausedTimer() @@ -285,7 +284,6 @@ class AudioPlayer: NSObject { } updateNowPlaying() - lastPlayTime = Date.timeIntervalSinceReferenceDate self.startPausedTimer() } From f5d1e992ef62f05b7f69ebefc206b79651c5f4ae Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 25 Aug 2022 18:46:13 -0400 Subject: [PATCH 31/66] Fix edge case where incorrect progress was tracked --- ios/App/Shared/player/AudioPlayer.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 7d743054..73f732d5 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -276,13 +276,15 @@ class AudioPlayer: NSObject { guard self.isInitialized() else { return } self.audioPlayer.pause() - self.status = 0 - self.rate = 0.0 Task { - await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: true, isStopping: true) + let wasPlaying = self.status > 0 + await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: wasPlaying, isStopping: true) } + self.status = 0 + self.rate = 0.0 + updateNowPlaying() self.startPausedTimer() From 79f09b334cc504aa6acf28c0d074c3533db2e446 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 25 Aug 2022 17:50:55 -0500 Subject: [PATCH 32/66] Add:Book library sort by published year, Fix:Check and set sort/filter when changing library to different media type --- components/modals/OrderModal.vue | 4 ++++ store/libraries.js | 4 +++- store/user.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/components/modals/OrderModal.vue b/components/modals/OrderModal.vue index c1f364b4..79d7e23e 100644 --- a/components/modals/OrderModal.vue +++ b/components/modals/OrderModal.vue @@ -39,6 +39,10 @@ export default { text: 'Author (Last, First)', value: 'media.metadata.authorNameLF' }, + { + text: 'Published Year', + value: 'media.metadata.publishedYear' + }, { text: 'Added At', value: 'addedAt' diff --git a/store/libraries.js b/store/libraries.js index 08b62fa9..af868897 100644 --- a/store/libraries.js +++ b/store/libraries.js @@ -22,7 +22,7 @@ export const getters = { } export const actions = { - fetch({ state, commit, rootState }, libraryId) { + fetch({ state, commit, dispatch, rootState }, libraryId) { if (!rootState.user || !rootState.user.user) { console.error('libraries/fetch - User not set') return false @@ -35,6 +35,8 @@ export const actions = { var filterData = data.filterdata var issues = data.issues || 0 + dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true }) + commit('addUpdate', library) commit('setLibraryIssues', issues) commit('setLibraryFilterData', filterData) diff --git a/store/user.js b/store/user.js index 4f76dfb2..7ae96ebd 100644 --- a/store/user.js +++ b/store/user.js @@ -48,6 +48,36 @@ export const getters = { } export const actions = { + // When changing libraries make sure sort and filter is still valid + checkUpdateLibrarySortFilter({ state, dispatch, commit }, mediaType) { + var settingsUpdate = {} + if (mediaType == 'podcast') { + if (state.settings.mobileOrderBy == 'media.metadata.authorName' || state.settings.mobileOrderBy == 'media.metadata.authorNameLF') { + settingsUpdate.mobileOrderBy = 'media.metadata.author' + } + if (state.settings.mobileOrderBy == 'media.duration') { + settingsUpdate.mobileOrderBy = 'media.numTracks' + } + if (state.settings.mobileOrderBy == 'media.metadata.publishedYear') { + settingsUpdate.mobileOrderBy = 'media.metadata.title' + } + var invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues'] + var filterByFirstPart = (state.settings.mobileFilterBy || '').split('.').shift() + if (invalidFilters.includes(filterByFirstPart)) { + settingsUpdate.filterBy = 'all' + } + } else { + if (state.settings.mobileOrderBy == 'media.metadata.author') { + settingsUpdate.mobileOrderBy = 'media.metadata.authorName' + } + if (state.settings.mobileOrderBy == 'media.numTracks') { + settingsUpdate.mobileOrderBy = 'media.duration' + } + } + if (Object.keys(settingsUpdate).length) { + dispatch('updateUserSettings', settingsUpdate) + } + }, async updateUserSettings({ state, commit }, payload) { if (state.serverConnectionConfig) { var updatePayload = { From 14ff9097c79738f1f0b7e94147314c1eb3f62663 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 25 Aug 2022 17:58:29 -0500 Subject: [PATCH 33/66] Update:Changing library while on item page redirects to home --- components/modals/LibrariesModal.vue | 3 ++- pages/item/_id.vue | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/components/modals/LibrariesModal.vue b/components/modals/LibrariesModal.vue index c7fc936e..e5b50905 100644 --- a/components/modals/LibrariesModal.vue +++ b/components/modals/LibrariesModal.vue @@ -50,8 +50,9 @@ export default { methods: { async clickedOption(lib) { this.show = false + if (lib.id === this.currentLibraryId) return await this.$store.dispatch('libraries/fetch', lib.id) - this.$eventBus.$emit('library-changed') + this.$eventBus.$emit('library-changed', lib.id) this.$localStore.setLastLibraryId(lib.id) } }, diff --git a/pages/item/_id.vue b/pages/item/_id.vue index 53e886f6..3a7a4b94 100644 --- a/pages/item/_id.vue +++ b/pages/item/_id.vue @@ -583,13 +583,20 @@ export default { this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) }) } + }, + libraryChanged(libraryId) { + if (this.libraryItem.libraryId !== libraryId) { + this.$router.replace('/bookshelf') + } } }, mounted() { + this.$eventBus.$on('library-changed', this.libraryChanged) this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem) this.$socket.on('item_updated', this.itemUpdated) }, beforeDestroy() { + this.$eventBus.$off('library-changed', this.libraryChanged) this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem) this.$socket.off('item_updated', this.itemUpdated) } From eb7a241e944d812be6f562a4d6033611d2f1c148 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 25 Aug 2022 19:03:05 -0400 Subject: [PATCH 34/66] Fix sleep timer chapter locking --- ios/App/Shared/player/AudioPlayer.swift | 2 +- ios/App/Shared/player/PlayerHandler.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 73f732d5..5ba8f9cb 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -413,6 +413,7 @@ class AudioPlayer: NSObject { sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: queue) { [weak self] in NSLog("SLEEP TIMER: Pausing audio") self?.pause() + PlayerHandler.sleepTimerChapterStopTime = nil self?.removeSleepTimer() } @@ -454,7 +455,6 @@ class AudioPlayer: NSObject { } public func removeSleepTimer() { - PlayerHandler.sleepTimerChapterStopTime = nil self.sleepTimeStopAt = nil if let token = sleepTimeToken { self.audioPlayer.removeTimeObserver(token) diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index 505c58b8..ae97120e 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -111,6 +111,7 @@ class PlayerHandler { } public static func cancelSleepTime() { + PlayerHandler.sleepTimerChapterStopTime = nil self.player?.removeSleepTimer() } From a5aae3fce8c84520be4671cc02457992e47c1685 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 25 Aug 2022 18:04:14 -0500 Subject: [PATCH 35/66] Fix:Audio player more menu close playback --- components/app/AudioPlayer.vue | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index 36eba3f7..e8353625 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -179,7 +179,7 @@ export default { icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank' }, { - text: 'Lock/Unlock', + text: this.lockUi ? 'Unlock Player' : 'Lock Player', value: 'lock', icon: this.lockUi ? 'lock' : 'lock_open' }, @@ -643,21 +643,21 @@ export default { }, clickMenuAction(action) { this.showMoreMenuDialog = false - if (action === 'lock') { - this.lockUi = !this.lockUi - this.$localStore.setPlayerLock(this.lockUi) - } else if (action === 'chapter_track') { - this.useChapterTrack = !this.useChapterTrack + this.$nextTick(() => { + if (action === 'lock') { + this.lockUi = !this.lockUi + this.$localStore.setPlayerLock(this.lockUi) + } else if (action === 'chapter_track') { + this.useChapterTrack = !this.useChapterTrack - this.$nextTick(() => { this.updateTimestamp() this.updateTrack() this.updateReadyTrack() - }) - this.$localStore.setUseChapterTrack(this.useChapterTrack) - } else if (action === 'close') { - this.closePlayback() - } + this.$localStore.setUseChapterTrack(this.useChapterTrack) + } else if (action === 'close') { + this.closePlayback() + } + }) }, forceCloseDropdownMenu() { if (this.$refs.dropdownMenu && this.$refs.dropdownMenu.closeMenu) { From 2076b93e1969f10059cb920c6fca9fc656009b1b Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 26 Aug 2022 18:35:47 -0400 Subject: [PATCH 36/66] Fix edge case when seeking past chapter --- ios/App/Shared/player/AudioPlayer.swift | 18 ++++++++++++++++++ ios/App/Shared/player/PlayerHandler.swift | 20 +++++++++++++------- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 5ba8f9cb..4287d897 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -423,8 +423,13 @@ class AudioPlayer: NSObject { private func rescheduleSleepTimerAtTime(time: Double, secondsRemaining: Int?) { // Not a chapter sleep timer + let hadToCancelChapterSleepTimer = decideIfChapterSleepTimerNeedsToBeCanceled(time: time) + guard !hadToCancelChapterSleepTimer else { return } guard PlayerHandler.sleepTimerChapterStopTime == nil else { return } + // Verify sleep timer is set + guard self.sleepTimeToken != nil else { return } + // Update the sleep timer if let secondsRemaining = secondsRemaining { let newSleepTimerPosition = time + Double(secondsRemaining) @@ -432,6 +437,19 @@ class AudioPlayer: NSObject { } } + private func decideIfChapterSleepTimerNeedsToBeCanceled(time: Double) -> Bool { + if let chapterSleepTime = PlayerHandler.sleepTimerChapterStopTime { + let sleepIsBeforeCurrentTime = Double(chapterSleepTime) <= time + if sleepIsBeforeCurrentTime { + PlayerHandler.sleepTimerChapterStopTime = nil + self.removeSleepTimer() + return true + } + } + + return false + } + public func increaseSleepTime(extraTimeInSeconds: Double) { if let sleepTime = PlayerHandler.remainingSleepTime { let currentTime = getCurrentTime() diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index ae97120e..c565690d 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -63,16 +63,22 @@ class PlayerHandler { get { guard let player = player else { return nil } - // Consider paused as playing at 1x - let rate = Double(player.rate > 0 ? player.rate : 1) - + // Return the player time until sleep + var timeUntilSleep: Double? = nil if let sleepTimerChapterStopTime = sleepTimerChapterStopTime { - let timeUntilChapterEnd = Double(sleepTimerChapterStopTime) - player.getCurrentTime() - let timeUntilChapterEndScaled = timeUntilChapterEnd / rate - return Int(timeUntilChapterEndScaled.rounded()) + timeUntilSleep = Double(sleepTimerChapterStopTime) - player.getCurrentTime() } else if let stopAt = player.getSleepStopAt() { - let timeUntilSleep = stopAt - player.getCurrentTime() + timeUntilSleep = stopAt - player.getCurrentTime() + } + + // Scale the time until sleep based on the playback rate + if let timeUntilSleep = timeUntilSleep { + // Consider paused as playing at 1x + let rate = Double(player.rate > 0 ? player.rate : 1) + let timeUntilSleepScaled = timeUntilSleep / rate + guard timeUntilSleepScaled.isNaN == false else { return nil } + return Int(timeUntilSleepScaled.rounded()) } else { return nil From 66ab402a50935540a511aa55550436daf78ad952 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 26 Aug 2022 18:58:08 -0400 Subject: [PATCH 37/66] Fix failed playback session on initAudioSession --- ios/App/Shared/player/AudioPlayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 6a16c494..36d328f2 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -332,7 +332,7 @@ class AudioPlayer: NSObject { private func initAudioSession() { do { - try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [.allowAirPlay]) + try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio) try AVAudioSession.sharedInstance().setActive(true) } catch { NSLog("Failed to set AVAudioSession category") From 3c6f29bf3a054c3259fea2363cbc143dbbe188a8 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 26 Aug 2022 19:11:55 -0400 Subject: [PATCH 38/66] Fix time observer crashing when already on main thread --- ios/App/Shared/player/AudioPlayer.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 4287d897..fa301781 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -137,14 +137,14 @@ class AudioPlayer: NSObject { private func setupTimeObserver() { // Time observer should be configured on the main queue - DispatchQueue.main.sync { + DispatchQueue.runOnMainQueue { self.removeTimeObserver() let timeScale = CMTimeScale(NSEC_PER_SEC) // Rate will be different depending on playback speed, aim for 2 observations/sec let seconds = 0.5 * (self.rate > 0 ? self.rate : 1.0) let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale) - self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: queue) { [weak self] time in + self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: self.queue) { [weak self] time in Task { // Let the player update the current playback positions await PlayerProgress.shared.syncFromPlayer(currentTime: time.seconds, includesPlayProgress: true, isStopping: false) From ac10997ed739b582b60261b655f320749d67c5f3 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 26 Aug 2022 19:34:34 -0400 Subject: [PATCH 39/66] Fix: Skip preferences not respected for iOS remote control --- ios/App/Shared/player/AudioPlayer.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 6a16c494..23dd3bba 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -346,6 +346,7 @@ class AudioPlayer: NSObject { UIApplication.shared.beginReceivingRemoteControlEvents() } let commandCenter = MPRemoteCommandCenter.shared() + let deviceSettings = Database.shared.getDeviceSettings() commandCenter.playCommand.isEnabled = true commandCenter.playCommand.addTarget { [unowned self] event in @@ -359,7 +360,7 @@ class AudioPlayer: NSObject { } commandCenter.skipForwardCommand.isEnabled = true - commandCenter.skipForwardCommand.preferredIntervals = [30] + commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: deviceSettings.jumpForwardTime)] commandCenter.skipForwardCommand.addTarget { [unowned self] event in guard let command = event.command as? MPSkipIntervalCommand else { return .noSuchContent @@ -369,7 +370,7 @@ class AudioPlayer: NSObject { return .success } commandCenter.skipBackwardCommand.isEnabled = true - commandCenter.skipBackwardCommand.preferredIntervals = [30] + commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: deviceSettings.jumpBackwardsTime)] commandCenter.skipBackwardCommand.addTarget { [unowned self] event in guard let command = event.command as? MPSkipIntervalCommand else { return .noSuchContent From ba1efedd79a1dd3729039a48d6773019e8cfe2b0 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 26 Aug 2022 19:48:54 -0400 Subject: [PATCH 40/66] Show currentTime in console --- ios/App/Shared/player/PlayerProgress.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/App/Shared/player/PlayerProgress.swift b/ios/App/Shared/player/PlayerProgress.swift index d99a15a4..c39cce8d 100644 --- a/ios/App/Shared/player/PlayerProgress.swift +++ b/ios/App/Shared/player/PlayerProgress.swift @@ -141,7 +141,7 @@ class PlayerProgress { session = session.freeze() guard safeToSync else { return } - NSLog("Sending sessionId(\(session.id)) to server") + NSLog("Sending sessionId(\(session.id)) to server with currentTime(\(session.currentTime))") var success = false if session.isLocal { From c1f803bdd06fb00818549a318c104bdcca9b4044 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 26 Aug 2022 20:04:06 -0400 Subject: [PATCH 41/66] Fix configs sharing an index --- ios/App/App/plugins/AbsDatabase.swift | 2 +- ios/App/Shared/util/Database.swift | 48 +++++++++++++++++---------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/ios/App/App/plugins/AbsDatabase.swift b/ios/App/App/plugins/AbsDatabase.swift index 3a281277..43e1969b 100644 --- a/ios/App/App/plugins/AbsDatabase.swift +++ b/ios/App/App/plugins/AbsDatabase.swift @@ -43,7 +43,7 @@ public class AbsDatabase: CAPPlugin { let config = ServerConnectionConfig() config.id = id ?? "" - config.index = 1 + config.index = 0 config.name = name config.address = address config.userId = userId diff --git a/ios/App/Shared/util/Database.swift b/ios/App/Shared/util/Database.swift index 721f7121..6aaa919d 100644 --- a/ios/App/Shared/util/Database.swift +++ b/ios/App/Shared/util/Database.swift @@ -20,26 +20,38 @@ class Database { let realm = try! Realm() let existing: ServerConnectionConfig? = realm.object(ofType: ServerConnectionConfig.self, forPrimaryKey: config.id) - if config.index == 0 { - let lastConfig: ServerConnectionConfig? = realm.objects(ServerConnectionConfig.self).last - - if lastConfig != nil { - config.index = lastConfig!.index + 1 - } else { - config.index = 1 - } - } - - do { - try realm.write { - if existing != nil { - realm.delete(existing!) + if let existing = existing { + do { + try existing.update { + existing.name = config.name + existing.address = config.address + existing.userId = config.userId + existing.username = config.username + existing.token = config.token } - realm.add(config) + } catch { + NSLog("failed to update server config") + debugPrint(error) + } + } else { + if config.index == 0 { + let lastConfig: ServerConnectionConfig? = realm.objects(ServerConnectionConfig.self).last + + if lastConfig != nil { + config.index = lastConfig!.index + 1 + } else { + config.index = 1 + } + } + + do { + try realm.write { + realm.add(config) + } + } catch(let exception) { + NSLog("failed to save server config") + debugPrint(exception) } - } catch(let exception) { - NSLog("failed to save server config") - debugPrint(exception) } setLastActiveConfigIndex(index: config.index) From eb7289c1509037faaf6173264663e8d0cbaedb44 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 26 Aug 2022 20:20:26 -0400 Subject: [PATCH 42/66] Fix the incorrect server config being persisted --- ios/App/App/plugins/AbsDatabase.swift | 3 ++- ios/App/Shared/util/Database.swift | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ios/App/App/plugins/AbsDatabase.swift b/ios/App/App/plugins/AbsDatabase.swift index 43e1969b..7f3d9f54 100644 --- a/ios/App/App/plugins/AbsDatabase.swift +++ b/ios/App/App/plugins/AbsDatabase.swift @@ -51,7 +51,8 @@ public class AbsDatabase: CAPPlugin { config.token = token Store.serverConfig = config - call.resolve(convertServerConnectionConfigToJSON(config: config)) + let savedConfig = Store.serverConfig // Fetch the latest value + call.resolve(convertServerConnectionConfigToJSON(config: savedConfig!)) } @objc func removeServerConnectionConfig(_ call: CAPPluginCall) { let id = call.getString("serverConnectionConfigId", "") diff --git a/ios/App/Shared/util/Database.swift b/ios/App/Shared/util/Database.swift index 6aaa919d..8fd116b8 100644 --- a/ios/App/Shared/util/Database.swift +++ b/ios/App/Shared/util/Database.swift @@ -33,6 +33,8 @@ class Database { NSLog("failed to update server config") debugPrint(error) } + + setLastActiveConfigIndex(index: existing.index) } else { if config.index == 0 { let lastConfig: ServerConnectionConfig? = realm.objects(ServerConnectionConfig.self).last @@ -52,9 +54,9 @@ class Database { NSLog("failed to save server config") debugPrint(exception) } + + setLastActiveConfigIndex(index: config.index) } - - setLastActiveConfigIndex(index: config.index) } public func deleteServerConnectionConfig(id: String) { From 1411157bde215e8297aa988dcae2384907de3efc Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 26 Aug 2022 20:20:46 -0400 Subject: [PATCH 43/66] Reindex server configs and fix bad data --- ios/App/App/AppDelegate.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index 6e93aab0..b37c9931 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -11,7 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. let configuration = Realm.Configuration( - schemaVersion: 3, + schemaVersion: 4, migrationBlock: { migration, oldSchemaVersion in if (oldSchemaVersion < 1) { NSLog("Realm schema version was \(oldSchemaVersion)") @@ -19,6 +19,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { newObject?["enableAltView"] = false } } + if (oldSchemaVersion < 4) { + NSLog("Realm schema version was \(oldSchemaVersion)... Reindexing server configs") + var indexCounter = 1 + migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in + newObject?["index"] = indexCounter + indexCounter += 1 + } + } } ) Realm.Configuration.defaultConfiguration = configuration From 64147d065ca646ec1b87a4e391517d94e3ca39d1 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 27 Aug 2022 15:54:13 -0500 Subject: [PATCH 44/66] Update:Book collection row item clickable #305 --- components/tables/collection/BookTableRow.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/tables/collection/BookTableRow.vue b/components/tables/collection/BookTableRow.vue index 54db2cbf..72ca46d8 100644 --- a/components/tables/collection/BookTableRow.vue +++ b/components/tables/collection/BookTableRow.vue @@ -1,17 +1,17 @@ From a2ac0e29e4627e3912a5af62e39bd30fbabb8d5c Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 27 Aug 2022 16:00:20 -0500 Subject: [PATCH 45/66] Version bump 0.9.57-beta --- android/app/build.gradle | 4 ++-- package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index e35667d0..01dac986 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,8 +33,8 @@ android { applicationId "com.audiobookshelf.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 87 - versionName "0.9.56-beta" + versionCode 88 + versionName "0.9.57-beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/package-lock.json b/package-lock.json index 8f27f60c..fdd727b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-app", - "version": "0.9.56-beta", + "version": "0.9.57-beta", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf-app", - "version": "0.9.56-beta", + "version": "0.9.57-beta", "dependencies": { "@capacitor/android": "^3.4.3", "@capacitor/app": "^1.1.1", @@ -32982,4 +32982,4 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index d4d7777f..d06284b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-app", - "version": "0.9.56-beta", + "version": "0.9.57-beta", "author": "advplyr", "scripts": { "dev": "nuxt --hostname 0.0.0.0 --port 1337", From 91dbd73d45d97a00ffd5f5fccd09f4b994666e01 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 27 Aug 2022 17:03:31 -0500 Subject: [PATCH 46/66] iOS version bump 0.9.57 --- ios/App/App.xcodeproj/project.pbxproj | 8 ++++---- ios/App/Podfile | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 2b6c149a..7271bb36 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -595,12 +595,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Icons; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEVELOPMENT_TEAM = 7UFJ7D8V6A; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.9.56; + MARKETING_VERSION = 0.9.57; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -619,12 +619,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Icons; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEVELOPMENT_TEAM = 7UFJ7D8V6A; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.9.56; + MARKETING_VERSION = 0.9.57; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/ios/App/Podfile b/ios/App/Podfile index 89125e80..dcdae730 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -9,12 +9,12 @@ install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' - pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app' - pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog' - pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics' - pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network' - pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar' - pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage' + pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' + pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog' + pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' + pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network' + pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' + pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage' end target 'App' do From 711b07996324f5dae514e5edf62ed5e6acbdd01a Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 29 Aug 2022 20:39:55 -0400 Subject: [PATCH 47/66] Fix: Extra timeListening when player initializes --- ios/App/Shared/player/AudioPlayer.swift | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 65d6ed2a..356d7bda 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -145,9 +145,11 @@ class AudioPlayer: NSObject { let seconds = 0.5 * (self.rate > 0 ? self.rate : 1.0) let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale) self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: self.queue) { [weak self] time in + let isPlaying = self?.isPlaying() ?? false + Task { // Let the player update the current playback positions - await PlayerProgress.shared.syncFromPlayer(currentTime: time.seconds, includesPlayProgress: true, isStopping: false) + await PlayerProgress.shared.syncFromPlayer(currentTime: time.seconds, includesPlayProgress: isPlaying, isStopping: false) } // Update the sleep time, if set @@ -257,8 +259,7 @@ class AudioPlayer: NSObject { self.stopPausedTimer() Task { - let isPlaying = self.status > 0 - await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: isPlaying, isStopping: false) + await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: self.isPlaying(), isStopping: false) } self.audioPlayer.play() @@ -278,8 +279,7 @@ class AudioPlayer: NSObject { self.audioPlayer.pause() Task { - let wasPlaying = self.status > 0 - await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: wasPlaying, isStopping: true) + await PlayerProgress.shared.syncFromPlayer(currentTime: self.getCurrentTime(), includesPlayProgress: self.isPlaying(), isStopping: true) } self.status = 0 @@ -495,14 +495,20 @@ class AudioPlayer: NSObject { let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)! return playbackSession.playMethod } + public func getPlaybackSessionId() -> String { return self.sessionId } + public func getDuration() -> Double { let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)! return playbackSession.duration } + public func isPlaying() -> Bool { + return self.status > 0 + } + // MARK: - Private private func createAsset(itemId:String, track:AudioTrack) -> AVAsset { let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)! From 9ee1203130f4370ce4cd97e5d59a04751dbbd4bf Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 29 Aug 2022 21:16:51 -0400 Subject: [PATCH 48/66] Fix multi-track currentTime value --- ios/App/Shared/player/AudioPlayer.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 356d7bda..3ffe4259 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -145,15 +145,18 @@ class AudioPlayer: NSObject { let seconds = 0.5 * (self.rate > 0 ? self.rate : 1.0) let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale) self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: self.queue) { [weak self] time in - let isPlaying = self?.isPlaying() ?? false + guard let self = self else { return } + + let currentTime = self.getCurrentTime() + let isPlaying = self.isPlaying() Task { // Let the player update the current playback positions - await PlayerProgress.shared.syncFromPlayer(currentTime: time.seconds, includesPlayProgress: isPlaying, isStopping: false) + await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: isPlaying, isStopping: false) } // Update the sleep time, if set - if self?.sleepTimeStopAt != nil { + if self.sleepTimeStopAt != nil { NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) } } From b583a8e6e2ead325dcd242abab242a1f634a1147 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 30 Aug 2022 17:05:06 -0400 Subject: [PATCH 49/66] Handle iOS crashes when no session --- ios/App/Shared/player/AudioPlayer.swift | 104 ++++++++++++++-------- ios/App/Shared/player/PlayerHandler.swift | 14 +-- 2 files changed, 78 insertions(+), 40 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 3ffe4259..bc69f9fb 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -63,15 +63,22 @@ class AudioPlayer: NSObject { initAudioSession() setupRemoteTransportControls() - let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)! + let playbackSession = self.getPlaybackSession() + guard let playbackSession = playbackSession else { + NSLog("Failed to fetch playback session. Player will not initialize") + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) + return + } // 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) + if let playerAsset = createAsset(itemId: playbackSession.libraryItemId!, track: track) { + let playerItem = AVPlayerItem(asset: playerAsset) + self.allPlayerItems.append(playerItem) + } } self.currentTrackIndex = getItemIndexForTime(time: playbackSession.currentTime) @@ -118,12 +125,16 @@ class AudioPlayer: NSObject { NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil) } - func isInitialized() -> Bool { + public func isInitialized() -> Bool { return self.status != -1 } - func getItemIndexForTime(time:Double) -> Int { - let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)! + 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 } for index in 0.. currentTime { self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) @@ -464,8 +492,7 @@ class AudioPlayer: NSObject { } public func decreaseSleepTime(removeTimeInSeconds: Double) { - if let sleepTime = PlayerHandler.remainingSleepTime { - let currentTime = getCurrentTime() + if let sleepTime = PlayerHandler.remainingSleepTime, let currentTime = getCurrentTime() { let newSleepTimerPosition = currentTime + Double(sleepTime) - removeTimeInSeconds guard newSleepTimerPosition > currentTime else { return } if newSleepTimerPosition > currentTime { @@ -486,16 +513,16 @@ class AudioPlayer: NSObject { NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self) } - public func getCurrentTime() -> Double { - let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)! + public func getCurrentTime() -> Double? { + guard let playbackSession = self.getPlaybackSession() else { return nil } let currentTrackTime = self.audioPlayer.currentTime().seconds let audioTrack = playbackSession.audioTracks[currentTrackIndex] let startOffset = audioTrack.startOffset ?? 0.0 return startOffset + currentTrackTime } - public func getPlayMethod() -> Int { - let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)! + public func getPlayMethod() -> Int? { + guard let playbackSession = self.getPlaybackSession() else { return nil } return playbackSession.playMethod } @@ -503,8 +530,8 @@ class AudioPlayer: NSObject { return self.sessionId } - public func getDuration() -> Double { - let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)! + public func getDuration() -> Double? { + guard let playbackSession = self.getPlaybackSession() else { return nil } return playbackSession.duration } @@ -513,8 +540,9 @@ class AudioPlayer: NSObject { } // MARK: - Private - private func createAsset(itemId:String, track:AudioTrack) -> AVAsset { - let playbackSession = Database.shared.getPlaybackSession(id: self.sessionId)! + private func createAsset(itemId:String, track:AudioTrack) -> AVAsset? { + guard let playbackSession = self.getPlaybackSession() else { return nil } + 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 ?? "" @@ -576,8 +604,10 @@ class AudioPlayer: NSObject { guard let command = event.command as? MPSkipIntervalCommand else { return .noSuchContent } - - seek(getCurrentTime() + command.preferredIntervals[0].doubleValue, from: "remote") + guard let currentTime = self.getCurrentTime() else { + return .commandFailed + } + seek(currentTime + command.preferredIntervals[0].doubleValue, from: "remote") return .success } commandCenter.skipBackwardCommand.isEnabled = true @@ -586,8 +616,10 @@ class AudioPlayer: NSObject { guard let command = event.command as? MPSkipIntervalCommand else { return .noSuchContent } - - seek(getCurrentTime() - command.preferredIntervals[0].doubleValue, from: "remote") + guard let currentTime = self.getCurrentTime() else { + return .commandFailed + } + seek(currentTime - command.preferredIntervals[0].doubleValue, from: "remote") return .success } @@ -614,7 +646,9 @@ class AudioPlayer: NSObject { } private func updateNowPlaying() { NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil) - NowPlayingInfo.shared.update(duration: getDuration(), currentTime: getCurrentTime(), rate: rate) + if let duration = self.getDuration(), let currentTime = self.getCurrentTime() { + NowPlayingInfo.shared.update(duration: duration, currentTime: currentTime, rate: rate) + } } // MARK: - Observer diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index c565690d..b2f49587 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -62,13 +62,14 @@ class PlayerHandler { public static var remainingSleepTime: Int? { get { guard let player = player else { return nil } + guard let currentTime = player.getCurrentTime() else { return nil } // Return the player time until sleep var timeUntilSleep: Double? = nil if let sleepTimerChapterStopTime = sleepTimerChapterStopTime { - timeUntilSleep = Double(sleepTimerChapterStopTime) - player.getCurrentTime() + timeUntilSleep = Double(sleepTimerChapterStopTime) - currentTime } else if let stopAt = player.getSleepStopAt() { - timeUntilSleep = stopAt - player.getCurrentTime() + timeUntilSleep = stopAt - currentTime } // Scale the time until sleep based on the playback rate @@ -96,7 +97,8 @@ class PlayerHandler { public static func setSleepTime(secondsUntilSleep: Double) { guard let player = player else { return } - let stopAt = secondsUntilSleep + player.getCurrentTime() + guard let currentTime = player.getCurrentTime() else { return } + let stopAt = secondsUntilSleep + currentTime player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: true) } @@ -134,15 +136,17 @@ class PlayerHandler { public static func seekForward(amount: Double) { guard let player = player else { return } + guard let currentTime = player.getCurrentTime() else { return } - let destinationTime = player.getCurrentTime() + amount + let destinationTime = currentTime + amount player.seek(destinationTime, from: "handler") } public static func seekBackward(amount: Double) { guard let player = player else { return } + guard let currentTime = player.getCurrentTime() else { return } - let destinationTime = player.getCurrentTime() - amount + let destinationTime = currentTime - amount player.seek(destinationTime, from: "handler") } From 35631fb211c62dcd54eba575e89c682eb38841c5 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 30 Aug 2022 22:14:59 -0400 Subject: [PATCH 50/66] feat: Handle iOS background downloads --- ios/App/App/AppDelegate.swift | 7 +++++++ ios/App/App/plugins/AbsDownloader.swift | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index b37c9931..01115c80 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -6,6 +6,7 @@ import RealmSwift class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? + var backgroundCompletionHandler: (() -> Void)? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. @@ -72,6 +73,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // tracking app url opens, make sure to keep this call return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) } + + func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { + // Stores the completion handler for background downloads + // The identifier of this method can be ignored at this time as we only have one background url session + backgroundCompletionHandler = completionHandler + } override func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index d430fd5e..22c42730 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -15,9 +15,10 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] private lazy var session: URLSession = { + let config = URLSessionConfiguration.background(withIdentifier: "AbsDownloader") let queue = OperationQueue() queue.maxConcurrentOperationCount = 5 - return URLSession(configuration: .default, delegate: self, delegateQueue: queue) + return URLSession(configuration: config, delegate: self, delegateQueue: queue) }() private let progressStatusQueue = DispatchQueue(label: "progress-status-queue", attributes: .concurrent) private var downloadItemProgress = [String: DownloadItem]() @@ -78,6 +79,18 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { } } + // Called when downloads are complete on the background thread + public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + DispatchQueue.main.async { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, + let backgroundCompletionHandler = + appDelegate.backgroundCompletionHandler else { + return + } + backgroundCompletionHandler() + } + } + private func handleDownloadTaskUpdate(downloadTask: URLSessionTask, progressHandler: DownloadProgressHandler) { do { guard let downloadItemPartId = downloadTask.taskDescription else { throw LibraryItemDownloadError.noTaskDescription } From 20d932877ed5614d6f4348d64a32217ccd2c01ef Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 30 Aug 2022 22:33:55 -0400 Subject: [PATCH 51/66] fix: Only mark audio session as active when playing --- ios/App/Shared/player/AudioPlayer.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index bc69f9fb..304181c0 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -284,7 +284,8 @@ class AudioPlayer: NSObject { await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: self.isPlaying(), isStopping: false) } } - + + self.markAudioSessionAs(active: true) self.audioPlayer.play() self.status = 1 self.rate = self.tmpRate @@ -302,6 +303,7 @@ class AudioPlayer: NSObject { guard self.isInitialized() else { return } self.audioPlayer.pause() + self.markAudioSessionAs(active: false) Task { if let currentTime = self.getCurrentTime() { @@ -572,13 +574,20 @@ class AudioPlayer: NSObject { private func initAudioSession() { do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio) - try AVAudioSession.sharedInstance().setActive(true) } catch { NSLog("Failed to set AVAudioSession category") print(error) } } + private func markAudioSessionAs(active: Bool) { + do { + try AVAudioSession.sharedInstance().setActive(active) + } catch { + NSLog("Failed to set audio session as active=\(active)") + } + } + // MARK: - Now playing private func setupRemoteTransportControls() { DispatchQueue.runOnMainQueue { From 11f22888d541de2ac749dead4f2c03088b08fa73 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 30 Aug 2022 22:47:55 -0400 Subject: [PATCH 52/66] feat: Handle resuming iOS audio after pause --- ios/App/Shared/player/AudioPlayer.swift | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 304181c0..f188b268 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -71,6 +71,7 @@ class AudioPlayer: NSObject { } // Listen to player events + self.setupInteruptionNotification() self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext) self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext) @@ -119,6 +120,7 @@ class AudioPlayer: NSObject { print(error) } + self.removeInteruptionNotification() DispatchQueue.runOnMainQueue { UIApplication.shared.endReceivingRemoteControlEvents() } @@ -146,6 +148,14 @@ class AudioPlayer: NSObject { return 0 } + private func setupInteruptionNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(handleInteruption), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance()) + } + + private func removeInteruptionNotification() { + NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance()) + } + private func setupTimeObserver() { // Time observer should be configured on the main queue DispatchQueue.runOnMainQueue { @@ -588,6 +598,25 @@ class AudioPlayer: NSObject { } } + // MARK: - iOS audio interupt notifications + @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 + } + + 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: () + } + } + // MARK: - Now playing private func setupRemoteTransportControls() { DispatchQueue.runOnMainQueue { From 82159ee537ede1a1ad0d0d2809fed7e59b755a63 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Tue, 30 Aug 2022 22:59:59 -0400 Subject: [PATCH 53/66] feat: Handle route change notifications --- ios/App/Shared/player/AudioPlayer.swift | 44 ++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index f188b268..87c9fadf 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -71,7 +71,7 @@ class AudioPlayer: NSObject { } // Listen to player events - self.setupInteruptionNotification() + self.setupAudioSessionNotifications() self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext) self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext) @@ -120,7 +120,7 @@ class AudioPlayer: NSObject { print(error) } - self.removeInteruptionNotification() + self.removeAudioSessionNotifications() DispatchQueue.runOnMainQueue { UIApplication.shared.endReceivingRemoteControlEvents() } @@ -148,12 +148,14 @@ class AudioPlayer: NSObject { return 0 } - private func setupInteruptionNotification() { + private func setupAudioSessionNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(handleInteruption), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance()) + NotificationCenter.default.addObserver(self, selector: #selector(handleRouteChange), name: AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance()) } - private func removeInteruptionNotification() { + private func removeAudioSessionNotifications() { NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance()) + NotificationCenter.default.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance()) } private func setupTimeObserver() { @@ -598,7 +600,7 @@ class AudioPlayer: NSObject { } } - // MARK: - iOS audio interupt notifications + // MARK: - iOS audio session notifications @objc private func handleInteruption(notification: Notification) { guard let userInfo = notification.userInfo, let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, @@ -617,6 +619,38 @@ class AudioPlayer: NSObject { } } + @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 + } + // MARK: - Now playing private func setupRemoteTransportControls() { DispatchQueue.runOnMainQueue { From 6257c6488ba69412e7f7841f54e8d9c3ce0a4ac7 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Wed, 31 Aug 2022 23:15:25 -0400 Subject: [PATCH 54/66] Fix race condition with the player queue --- ios/App/Shared/player/AudioPlayer.swift | 66 +++++++++++++++---------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index bc69f9fb..5bb5e962 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -195,36 +195,48 @@ class AudioPlayer: NSObject { private func setupQueueItemStatusObserver() { self.queueItemStatusObserver?.invalidate() - self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in - guard let playbackSession = self.getPlaybackSession() else { - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) - return - } - if (playerItem.status == .readyToPlay) { - NSLog("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)") - self.updateNowPlaying() - - // Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session - let firstReady = self.status < 0 - if firstReady || self.playWhenReady { - self.seek(playbackSession.currentTime, from: "queueItemStatusObserver") - } - - // Mark the player as ready - self.status = 0 - - // Start the player, if requested - if self.playWhenReady { - self.playWhenReady = false - self.play() - } - } else if (playerItem.status == .failed) { - NSLog("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")") - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) - } + let status = self.audioPlayer.currentItem?.status.rawValue ?? -1 + + NSLog("queueStatusObserver: Setting up status=\(status)") + // First item already loaded, we need to fire manually + if status == 1, let playerItem = self.audioPlayer.currentItem { + self.handleQueueItemStatus(playerItem: playerItem) + } + // Now listen for future updates + self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { playerItem, change in + self.handleQueueItemStatus(playerItem: playerItem) }) } + private func handleQueueItemStatus(playerItem: AVPlayerItem) { + NSLog("queueStatusObserver: Current item status changed") + guard let playbackSession = self.getPlaybackSession() else { + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) + return + } + if (playerItem.status == .readyToPlay) { + NSLog("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)") + + // Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session + let firstReady = self.status < 0 + if firstReady || self.playWhenReady { + self.seek(playbackSession.currentTime, from: "queueItemStatusObserver") + } + + // Mark the player as ready + self.status = 0 + + // Start the player, if requested + if self.playWhenReady { + self.playWhenReady = false + self.play() + } + } else if (playerItem.status == .failed) { + NSLog("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")") + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) + } + } + private func startPausedTimer() { guard self.pausedTimer == nil else { return } self.queue.async { From 50f6b15d233336d992d7f6f14c9b1bc5e21106bc Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 1 Sep 2022 17:40:49 -0400 Subject: [PATCH 55/66] Refactor to ensure we aren't missing any update time --- ios/App/Shared/player/AudioPlayer.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 5bb5e962..b20d69d7 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -194,18 +194,18 @@ class AudioPlayer: NSObject { } private func setupQueueItemStatusObserver() { + NSLog("queueStatusObserver: Setting up") + + // Listen for player item updates self.queueItemStatusObserver?.invalidate() - let status = self.audioPlayer.currentItem?.status.rawValue ?? -1 - - NSLog("queueStatusObserver: Setting up status=\(status)") - // First item already loaded, we need to fire manually - if status == 1, let playerItem = self.audioPlayer.currentItem { - self.handleQueueItemStatus(playerItem: playerItem) - } - // Now listen for future updates self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { playerItem, change in self.handleQueueItemStatus(playerItem: playerItem) }) + + // Ensure we didn't miss a player item update during initialization + if let playerItem = self.audioPlayer.currentItem { + self.handleQueueItemStatus(playerItem: playerItem) + } } private func handleQueueItemStatus(playerItem: AVPlayerItem) { From 021d63d973f2382996b1d5af14df1c4468018050 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 1 Sep 2022 18:19:45 -0400 Subject: [PATCH 56/66] Fix seek jumping --- ios/App/Shared/player/AudioPlayer.swift | 96 ++++++++++++++----------- 1 file changed, 53 insertions(+), 43 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index b20d69d7..3341d4f1 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -219,7 +219,9 @@ class AudioPlayer: NSObject { // Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session let firstReady = self.status < 0 - if firstReady || self.playWhenReady { + if firstReady || !self.playWhenReady { + // 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() self.seek(playbackSession.currentTime, from: "queueItemStatusObserver") } @@ -255,59 +257,67 @@ class AudioPlayer: NSObject { // MARK: - Methods public func play(allowSeekBack: Bool = false) { guard self.isInitialized() else { return } + guard let session = self.getPlaybackSession() else { + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) + return + } // Capture remaining sleep time before changing the track position let sleepSecondsRemaining = PlayerHandler.remainingSleepTime - if allowSeekBack, let session = self.getPlaybackSession() { - let lastPlayed = (session.updatedAt ?? 0)/1000 - let difference = Date.timeIntervalSinceReferenceDate - lastPlayed - var time: Int? - - if lastPlayed == 0 { - time = 5 - } else if difference < 6 { - time = 2 - } else if difference < 12 { - time = 10 - } else if difference < 30 { - time = 15 - } else if difference < 180 { - time = 20 - } else if difference < 3600 { - time = 25 - } else { - time = 29 - } - - if time != nil { - guard let currentTime = self.getCurrentTime() else { - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) - return - } - seek(currentTime - Double(time!), from: "play") - } - } - + // Stop the paused timer self.stopPausedTimer() - Task { - if let currentTime = self.getCurrentTime() { - await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: self.isPlaying(), isStopping: false) + // Determine where we are starting playback + let lastPlayed = (session.updatedAt ?? 0)/1000 + let currentTime = allowSeekBack ? calculateSeekBackTimeAtCurrentTime(session.currentTime, lastPlayed: lastPlayed) : session.currentTime + + // 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 + self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] completed in + if completed, let self = self { + // Start playback + self.audioPlayer.play() + self.rate = self.tmpRate + self.audioPlayer.rate = self.tmpRate + self.status = 1 + + // If we have an active sleep timer, reschedule based on rate + self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining) + + // Update the progress + self.updateNowPlaying() } } - - self.audioPlayer.play() - self.status = 1 - self.rate = self.tmpRate - self.audioPlayer.rate = self.tmpRate + } + + private func calculateSeekBackTimeAtCurrentTime(_ currentTime: Double, lastPlayed: Double) -> Double { + let difference = Date.timeIntervalSinceReferenceDate - lastPlayed + var time: Double = 0 - // If we have an active sleep timer, reschedule based on rate - if let currentTime = self.getCurrentTime() { - self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining) + // Scale seek back time based on how long since last play + if lastPlayed == 0 { + time = 5 + } else if difference < 6 { + time = 2 + } else if difference < 12 { + time = 10 + } else if difference < 30 { + time = 15 + } else if difference < 180 { + time = 20 + } else if difference < 3600 { + time = 25 + } else { + time = 29 } - updateNowPlaying() + // Wind the clock back + return currentTime - time } public func pause() { From c02adfa32615ce0cf62aa4fbea4c79bff686babe Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 1 Sep 2022 18:41:21 -0400 Subject: [PATCH 57/66] Fix typo in if statement --- ios/App/Shared/player/AudioPlayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 3341d4f1..affc56b2 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -219,7 +219,7 @@ class AudioPlayer: NSObject { // Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session let firstReady = self.status < 0 - if firstReady || !self.playWhenReady { + if firstReady && !self.playWhenReady { // 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() self.seek(playbackSession.currentTime, from: "queueItemStatusObserver") From 586405f9de3c77de0e10ee8885d66e0539f3b99d Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 2 Sep 2022 16:31:47 -0400 Subject: [PATCH 58/66] Rewrite sleep timer --- ios/App/App.xcodeproj/project.pbxproj | 4 + ios/App/App/plugins/AbsAudioPlayer.swift | 19 +- ios/App/Shared/player/AudioPlayer.swift | 178 ++++-------------- .../Shared/player/AudioPlayerSleepTimer.swift | 155 +++++++++++++++ ios/App/Shared/player/PlayerHandler.swift | 46 +---- 5 files changed, 209 insertions(+), 193 deletions(-) create mode 100644 ios/App/Shared/player/AudioPlayerSleepTimer.swift diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 7271bb36..1f223fbf 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ E9D5507128AC1EC700C746DD /* DownloadItemPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507028AC1EC700C746DD /* DownloadItemPart.swift */; }; E9D5507328AC218300C746DD /* DaoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507228AC218300C746DD /* DaoExtensions.swift */; }; E9D5507528AEF93100C746DD /* PlayerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507428AEF93100C746DD /* PlayerSettings.swift */; }; + E9DFCBFB28C28F4A00B36356 /* AudioPlayerSleepTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9DFCBFA28C28F4A00B36356 /* AudioPlayerSleepTimer.swift */; }; E9E985F828B02D9400957F23 /* PlayerProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E985F728B02D9400957F23 /* PlayerProgress.swift */; }; /* End PBXBuildFile section */ @@ -117,6 +118,7 @@ E9D5507028AC1EC700C746DD /* DownloadItemPart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadItemPart.swift; sourceTree = ""; }; E9D5507228AC218300C746DD /* DaoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaoExtensions.swift; sourceTree = ""; }; E9D5507428AEF93100C746DD /* PlayerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSettings.swift; sourceTree = ""; }; + E9DFCBFA28C28F4A00B36356 /* AudioPlayerSleepTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerSleepTimer.swift; sourceTree = ""; }; E9E985F728B02D9400957F23 /* PlayerProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerProgress.swift; sourceTree = ""; }; FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -145,6 +147,7 @@ isa = PBXGroup; children = ( 3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */, + E9DFCBFA28C28F4A00B36356 /* AudioPlayerSleepTimer.swift */, 3ABF618E2804325C0070250E /* PlayerHandler.swift */, E9E985F728B02D9400957F23 /* PlayerProgress.swift */, ); @@ -438,6 +441,7 @@ E9D5506028AC1CA900C746DD /* PlaybackMetadata.swift in Sources */, E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */, E9D5504E28AC1B0700C746DD /* AudioFile.swift in Sources */, + E9DFCBFB28C28F4A00B36356 /* AudioPlayerSleepTimer.swift in Sources */, E9D5505428AC1B7900C746DD /* AudioTrack.swift in Sources */, E9D5505C28AC1C6200C746DD /* LibraryFile.swift in Sources */, 4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */, diff --git a/ios/App/App/plugins/AbsAudioPlayer.swift b/ios/App/App/plugins/AbsAudioPlayer.swift index 44b3b374..0ec2d225 100644 --- a/ios/App/App/plugins/AbsAudioPlayer.swift +++ b/ios/App/App/plugins/AbsAudioPlayer.swift @@ -169,7 +169,7 @@ public class AbsAudioPlayer: CAPPlugin { @objc func decreaseSleepTime(_ call: CAPPluginCall) { guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let time = Double(timeString) else { return call.resolve([ "success": false ]) } - guard let _ = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) } + guard let _ = PlayerHandler.getSleepTimeRemaining() else { return call.resolve([ "success": false ]) } let seconds = time/1000 PlayerHandler.decreaseSleepTime(decreaseSeconds: seconds) @@ -179,7 +179,7 @@ public class AbsAudioPlayer: CAPPlugin { @objc func increaseSleepTime(_ call: CAPPluginCall) { guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let time = Double(timeString) else { return call.resolve([ "success": false ]) } - guard let _ = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) } + guard let _ = PlayerHandler.getSleepTimeRemaining() else { return call.resolve([ "success": false ]) } let seconds = time/1000 PlayerHandler.increaseSleepTime(increaseSeconds: seconds) @@ -188,30 +188,29 @@ public class AbsAudioPlayer: CAPPlugin { @objc func setSleepTimer(_ call: CAPPluginCall) { guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } - guard let time = Int(timeString) else { return call.resolve([ "success": false ]) } + guard let time = Double(timeString) else { return call.resolve([ "success": false ]) } let isChapterTime = call.getBool("isChapterTime", false) let seconds = time / 1000 NSLog("chapter time: \(isChapterTime)") if isChapterTime { - PlayerHandler.setChapterSleepTime(stopAt: Double(seconds)) + PlayerHandler.setChapterSleepTime(stopAt: seconds) return call.resolve([ "success": true ]) + } else { + PlayerHandler.setSleepTime(secondsUntilSleep: seconds) + call.resolve([ "success": true ]) } - - PlayerHandler.setSleepTime(secondsUntilSleep: Double(seconds)) - call.resolve([ "success": true ]) } @objc func cancelSleepTimer(_ call: CAPPluginCall) { PlayerHandler.cancelSleepTime() - PlayerHandler.sleepTimerChapterStopTime = nil call.resolve() } @objc func getSleepTimerTime(_ call: CAPPluginCall) { call.resolve([ - "value": PlayerHandler.remainingSleepTime + "value": PlayerHandler.getSleepTimeRemaining() ]) } @@ -223,7 +222,7 @@ public class AbsAudioPlayer: CAPPlugin { @objc func sendSleepTimerSet() { self.notifyListeners("onSleepTimerSet", data: [ - "value": PlayerHandler.remainingSleepTime + "value": PlayerHandler.getSleepTimeRemaining() ]) } diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index affc56b2..b91c66ce 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -18,7 +18,7 @@ enum PlayMethod:Int { } class AudioPlayer: NSObject { - private let queue = DispatchQueue(label: "ABSAudioPlayerQueue") + internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue") // enums and @objc are not compatible @objc dynamic var status: Int @@ -32,15 +32,17 @@ class AudioPlayer: NSObject { private var playWhenReady: Bool private var initialPlaybackRate: Float - private var audioPlayer: AVQueuePlayer + internal var audioPlayer: AVQueuePlayer private var sessionId: String private var timeObserverToken: Any? private var queueObserver:NSKeyValueObservation? private var queueItemStatusObserver:NSKeyValueObservation? - private var sleepTimeStopAt: Double? - private var sleepTimeToken: Any? + // Sleep timer values + internal var sleepTimeChapterStopAt: Double? + internal var sleepTimeStopAt: Double? + internal var sleepTimeToken: Any? private var currentTrackIndex = 0 private var allPlayerItems:[AVPlayerItem] = [] @@ -263,7 +265,7 @@ class AudioPlayer: NSObject { } // Capture remaining sleep time before changing the track position - let sleepSecondsRemaining = PlayerHandler.remainingSleepTime + let sleepSecondsRemaining = self.getSleepTimeRemaining() // Stop the paused timer self.stopPausedTimer() @@ -279,19 +281,17 @@ class AudioPlayer: NSObject { let currentTrackStartOffset = session.audioTracks[self.currentTrackIndex].startOffset ?? 0.0 let seekTime = currentTime - currentTrackStartOffset self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] completed in - if completed, let self = self { - // Start playback - self.audioPlayer.play() - self.rate = self.tmpRate - self.audioPlayer.rate = self.tmpRate - self.status = 1 - - // If we have an active sleep timer, reschedule based on rate - self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining) - - // Update the progress - self.updateNowPlaying() - } + guard completed else { return } + guard let self = self else { return } + + // Start playback + self.audioPlayer.play() + self.rate = self.tmpRate + self.audioPlayer.rate = self.tmpRate + self.status = 1 + + // Update the progress + self.updateNowPlaying() } } @@ -342,22 +342,22 @@ class AudioPlayer: NSObject { public func seek(_ to: Double, from: String) { let continuePlaying = rate > 0.0 + // Capture remaining sleep time before changing the track position or pausing + let sleepSecondsRemaining = self.getSleepTimeRemaining() + pause() - NSLog("Seek to \(to) from \(from)") + NSLog("SEEK: Seek to \(to) from \(from)") guard let playbackSession = self.getPlaybackSession() else { return } let currentTrack = playbackSession.audioTracks[self.currentTrackIndex] let ctso = currentTrack.startOffset ?? 0.0 let trackEnd = ctso + currentTrack.duration - NSLog("Seek current track END = \(trackEnd)") - - // Capture remaining sleep time before changing the track position - let sleepSecondsRemaining = PlayerHandler.remainingSleepTime + NSLog("SEEK: Seek current track END = \(trackEnd)") let indexOfSeek = getItemIndexForTime(time: to) - NSLog("Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)") + NSLog("SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)") // Reconstruct queue if seeking to a different track if (self.currentTrackIndex != indexOfSeek) { @@ -378,32 +378,30 @@ class AudioPlayer: NSObject { setupQueueItemStatusObserver() } else { - NSLog("Seeking in current item \(to)") + NSLog("SEEK: Seeking in current item \(to)") let currentTrackStartOffset = playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0 let seekTime = to - currentTrackStartOffset self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in - if !completed { - NSLog("WARNING: seeking not completed (to \(seekTime)") + guard completed else { return NSLog("SEEK: WARNING: seeking not completed (to \(seekTime)") } + guard let self = self else { return } + + // Reschedule the sleep timer + if let currentTime = self.getCurrentTime() { + self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining) } if continuePlaying { - self?.play() - } - self?.updateNowPlaying() - - // If we have an active sleep timer, reschedule based on seek, since seek is fuzzy - // This needs to occur after play() to capture the correct playback rate - if let currentTime = self?.getCurrentTime() { - self?.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining) + self.play() } + self.updateNowPlaying() } } } public func setPlaybackRate(_ rate: Float, observed: Bool = false) { // Capture remaining sleep time before changing the rate - let sleepSecondsRemaining = PlayerHandler.remainingSleepTime + let sleepSecondsRemaining = self.getSleepTimeRemaining() let playbackSpeedChanged = rate > 0.0 && rate != self.tmpRate && !(observed && rate == 1) if self.audioPlayer.rate != rate { @@ -427,114 +425,6 @@ class AudioPlayer: NSObject { } } - public func getSleepStopAt() -> Double? { - return self.sleepTimeStopAt - } - - // Let iOS handle the sleep timer logic by letting us know when it's time to stop - public func setSleepTime(stopAt: Double, scaleBasedOnSpeed: Bool = false) { - NSLog("SLEEP TIMER: Scheduling for \(stopAt)") - - // Reset any previous sleep timer - self.removeSleepTimer() - - guard let currentTime = getCurrentTime() else { - NSLog("Failed to get currenTime") - return - } - - // Mark the time to stop playing - if scaleBasedOnSpeed { - // Consider paused as playing at 1x - let rate = Double(self.rate > 0 ? self.rate : 1) - - // Calculate the scaled time to stop at - let timeUntilSleep = (stopAt - currentTime) * rate - self.sleepTimeStopAt = currentTime + timeUntilSleep - - NSLog("SLEEP TIMER: Adjusted based on playback speed of \(rate) to \(self.sleepTimeStopAt!)") - } else { - self.sleepTimeStopAt = stopAt - } - - guard let sleepTimeStopAt = self.sleepTimeStopAt else { return } - let sleepTime = CMTime(seconds: sleepTimeStopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - - // Schedule the observation time - var times = [NSValue]() - times.append(NSValue(time: sleepTime)) - - sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: queue) { [weak self] in - NSLog("SLEEP TIMER: Pausing audio") - self?.pause() - PlayerHandler.sleepTimerChapterStopTime = nil - self?.removeSleepTimer() - } - - // Update the UI - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) - } - - private func rescheduleSleepTimerAtTime(time: Double, secondsRemaining: Int?) { - // Not a chapter sleep timer - let hadToCancelChapterSleepTimer = decideIfChapterSleepTimerNeedsToBeCanceled(time: time) - guard !hadToCancelChapterSleepTimer else { return } - guard PlayerHandler.sleepTimerChapterStopTime == nil else { return } - - // Verify sleep timer is set - guard self.sleepTimeToken != nil else { return } - - // Update the sleep timer - if let secondsRemaining = secondsRemaining { - let newSleepTimerPosition = time + Double(secondsRemaining) - self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) - } - } - - private func decideIfChapterSleepTimerNeedsToBeCanceled(time: Double) -> Bool { - if let chapterSleepTime = PlayerHandler.sleepTimerChapterStopTime { - let sleepIsBeforeCurrentTime = Double(chapterSleepTime) <= time - if sleepIsBeforeCurrentTime { - PlayerHandler.sleepTimerChapterStopTime = nil - self.removeSleepTimer() - return true - } - } - - return false - } - - public func increaseSleepTime(extraTimeInSeconds: Double) { - if let sleepTime = PlayerHandler.remainingSleepTime, let currentTime = getCurrentTime() { - let newSleepTimerPosition = currentTime + Double(sleepTime) + extraTimeInSeconds - if newSleepTimerPosition > currentTime { - self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) - } - } - } - - public func decreaseSleepTime(removeTimeInSeconds: Double) { - if let sleepTime = PlayerHandler.remainingSleepTime, let currentTime = getCurrentTime() { - let newSleepTimerPosition = currentTime + Double(sleepTime) - removeTimeInSeconds - guard newSleepTimerPosition > currentTime else { return } - if newSleepTimerPosition > currentTime { - self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) - } - } - - } - - public func removeSleepTimer() { - self.sleepTimeStopAt = nil - if let token = sleepTimeToken { - self.audioPlayer.removeTimeObserver(token) - sleepTimeToken = nil - } - - // Update the UI - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self) - } - public func getCurrentTime() -> Double? { guard let playbackSession = self.getPlaybackSession() else { return nil } let currentTrackTime = self.audioPlayer.currentTime().seconds diff --git a/ios/App/Shared/player/AudioPlayerSleepTimer.swift b/ios/App/Shared/player/AudioPlayerSleepTimer.swift new file mode 100644 index 00000000..85839984 --- /dev/null +++ b/ios/App/Shared/player/AudioPlayerSleepTimer.swift @@ -0,0 +1,155 @@ +// +// AudioPlayerSleepTimer.swift +// App +// +// Created by Ron Heft on 9/2/22. +// + +import Foundation +import AVFoundation + +extension AudioPlayer { + + // MARK: - Public API + + public func getSleepTimeRemaining() -> Double? { + guard let currentTime = self.getCurrentTime() else { return nil } + + // Return the player time until sleep + var timeUntilSleep: Double? = nil + if let chapterStopAt = self.sleepTimeChapterStopAt { + timeUntilSleep = chapterStopAt - currentTime + } else if let stopAt = self.sleepTimeStopAt { + timeUntilSleep = stopAt - currentTime + } + + // Scale the time until sleep based on the playback rate + if let timeUntilSleep = timeUntilSleep { + let timeUntilSleepScaled = timeUntilSleep / self.getPlaybackRate() + guard timeUntilSleepScaled.isNaN == false else { return nil } + + return timeUntilSleepScaled.rounded() + } else { + return nil + } + } + + // Let iOS handle the sleep timer logic by letting us know when it's time to stop + public func setSleepTime(stopAt: Double, scaleBasedOnSpeed: Bool = false) { + NSLog("SLEEP TIMER: Scheduling for \(stopAt)") + + // Reset any previous sleep timer + let isChapterSleepTimer = !scaleBasedOnSpeed + self.removeSleepTimer(resetStopAt: !isChapterSleepTimer) + + guard let currentTime = getCurrentTime() else { + NSLog("SLEEP TIMER: Failed to get currenTime") + return + } + + // Mark the time to stop playing + let scaledStopAt = self.calculateScaledStopAt(stopAt, currentTime: currentTime, scaleBasedOnSpeed: scaleBasedOnSpeed) + self.sleepTimeStopAt = scaledStopAt + let sleepTime = CMTime(seconds: scaledStopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + + // Schedule the observation time + var times = [NSValue]() + times.append(NSValue(time: sleepTime)) + + self.sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: self.queue) { [weak self] in + NSLog("SLEEP TIMER: Pausing audio") + self?.pause() + self?.removeSleepTimer() + } + + // Update the UI + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) + } + + public func increaseSleepTime(extraTimeInSeconds: Double) { + if let sleepTime = self.getSleepTimeRemaining(), let currentTime = getCurrentTime() { + let newSleepTimerPosition = currentTime + sleepTime + extraTimeInSeconds + if newSleepTimerPosition > currentTime { + self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) + } + } + } + + public func decreaseSleepTime(removeTimeInSeconds: Double) { + if let sleepTime = self.getSleepTimeRemaining(), let currentTime = getCurrentTime() { + let newSleepTimerPosition = currentTime + sleepTime - removeTimeInSeconds + if newSleepTimerPosition > currentTime { + self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) + } + } + + } + + public func removeSleepTimer(resetStopAt: Bool = true) { + if resetStopAt { + self.sleepTimeStopAt = nil + self.sleepTimeChapterStopAt = nil + } + + if let token = self.sleepTimeToken { + self.audioPlayer.removeTimeObserver(token) + self.sleepTimeToken = nil + } + + // Update the UI + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self) + } + + + // MARK: - Internal helpers + + internal func rescheduleSleepTimerAtTime(time: Double, secondsRemaining: Double?) { + guard self.isSleepTimerSet() else { return } + + // Cancel a chapter sleep timer that is no longer valid + if isChapterSleepTimerBeforeTime(time) { + return self.removeSleepTimer() + } + + // Update the sleep timer + if !isChapterSleepTimer() { + guard let secondsRemaining = secondsRemaining else { return } + let newSleepTimerPosition = time + secondsRemaining + self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) + } + } + + private func isChapterSleepTimerBeforeTime(_ time: Double) -> Bool { + if let chapterStopAt = self.sleepTimeChapterStopAt { + return chapterStopAt <= time + } + + return false + } + + private func isSleepTimerSet() -> Bool { + return self.sleepTimeStopAt != nil + } + + private func isChapterSleepTimer() -> Bool { + return self.sleepTimeChapterStopAt != nil + } + + private func getPlaybackRate() -> Double { + // Consider paused as playing at 1x + return Double(self.rate > 0 ? self.rate : 1) + } + + private func calculateScaledStopAt(_ stopAt: Double, currentTime: Double, scaleBasedOnSpeed: Bool) -> Double { + if scaleBasedOnSpeed { + // Calculate the scaled time to stop at + let secondsUntilStopAt1x = stopAt - currentTime + let secondsUntilSleep = secondsUntilStopAt1x * self.getPlaybackRate() + NSLog("SLEEP TIMER: Adjusted based on playback speed of \(self.getPlaybackRate()) to \(secondsUntilSleep)") + return currentTime + secondsUntilSleep + } else { + return stopAt + } + } + +} diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index b2f49587..f11bf2f5 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -11,8 +11,6 @@ import RealmSwift class PlayerHandler { private static var player: AudioPlayer? - public static var sleepTimerChapterStopTime: Int? = nil - public static func startPlayback(sessionId: String, playWhenReady: Bool, playbackRate: Float) { guard let session = Database.shared.getPlaybackSession(id: sessionId) else { return } @@ -59,34 +57,6 @@ class PlayerHandler { } } - public static var remainingSleepTime: Int? { - get { - guard let player = player else { return nil } - guard let currentTime = player.getCurrentTime() else { return nil } - - // Return the player time until sleep - var timeUntilSleep: Double? = nil - if let sleepTimerChapterStopTime = sleepTimerChapterStopTime { - timeUntilSleep = Double(sleepTimerChapterStopTime) - currentTime - } else if let stopAt = player.getSleepStopAt() { - timeUntilSleep = stopAt - currentTime - } - - // Scale the time until sleep based on the playback rate - if let timeUntilSleep = timeUntilSleep { - // Consider paused as playing at 1x - let rate = Double(player.rate > 0 ? player.rate : 1) - - let timeUntilSleepScaled = timeUntilSleep / rate - guard timeUntilSleepScaled.isNaN == false else { return nil } - - return Int(timeUntilSleepScaled.rounded()) - } else { - return nil - } - } - } - public static func getCurrentTime() -> Double? { self.player?.getCurrentTime() } @@ -95,31 +65,29 @@ class PlayerHandler { self.player?.setPlaybackRate(speed) } + public static func getSleepTimeRemaining() -> Double? { + return self.player?.getSleepTimeRemaining() + } + public static func setSleepTime(secondsUntilSleep: Double) { - guard let player = player else { return } - guard let currentTime = player.getCurrentTime() else { return } + guard let currentTime = self.player?.getCurrentTime() else { return } let stopAt = secondsUntilSleep + currentTime - player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: true) + self.player?.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: true) } public static func setChapterSleepTime(stopAt: Double) { - guard let player = player else { return } - self.sleepTimerChapterStopTime = Int(stopAt) - player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: false) + self.player?.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: false) } public static func increaseSleepTime(increaseSeconds: Double) { - self.sleepTimerChapterStopTime = nil self.player?.increaseSleepTime(extraTimeInSeconds: increaseSeconds) } public static func decreaseSleepTime(decreaseSeconds: Double) { - self.sleepTimerChapterStopTime = nil self.player?.decreaseSleepTime(removeTimeInSeconds: decreaseSeconds) } public static func cancelSleepTime() { - PlayerHandler.sleepTimerChapterStopTime = nil self.player?.removeSleepTimer() } From dc8852eb0dd885905175ab16ee943d4c23310548 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 2 Sep 2022 16:52:08 -0400 Subject: [PATCH 59/66] Guard against seeking during initialization --- ios/App/Shared/player/PlayerHandler.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index f11bf2f5..a41414fb 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -104,6 +104,7 @@ class PlayerHandler { public static func seekForward(amount: Double) { guard let player = player else { return } + guard player.isInitialized() else { return } guard let currentTime = player.getCurrentTime() else { return } let destinationTime = currentTime + amount @@ -112,6 +113,7 @@ class PlayerHandler { public static func seekBackward(amount: Double) { guard let player = player else { return } + guard player.isInitialized() else { return } guard let currentTime = player.getCurrentTime() else { return } let destinationTime = currentTime - amount @@ -119,7 +121,10 @@ class PlayerHandler { } public static func seek(amount: Double) { - player?.seek(amount, from: "handler") + guard let player = player else { return } + guard player.isInitialized() else { return } + + player.seek(amount, from: "handler") } public static func getMetdata() -> [String: Any]? { From c14f6ec4c24f11843195ce4f07f5771a53035646 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 2 Sep 2022 18:22:42 -0400 Subject: [PATCH 60/66] Rewrite sleep timer logic again --- ios/App/Shared/player/AudioPlayer.swift | 62 +++---- .../Shared/player/AudioPlayerSleepTimer.swift | 172 +++++++++--------- ios/App/Shared/player/PlayerHandler.swift | 6 +- 3 files changed, 111 insertions(+), 129 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index b91c66ce..78b66bd9 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -41,8 +41,9 @@ class AudioPlayer: NSObject { // Sleep timer values internal var sleepTimeChapterStopAt: Double? - internal var sleepTimeStopAt: Double? - internal var sleepTimeToken: Any? + internal var sleepTimeChapterToken: Any? + internal var sleepTimer: Timer? + internal var sleepTimeRemaining: Double? private var currentTrackIndex = 0 private var allPlayerItems:[AVPlayerItem] = [] @@ -168,8 +169,8 @@ class AudioPlayer: NSObject { await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: isPlaying, isStopping: false) } - // Update the sleep time, if set - if self.sleepTimeStopAt != nil { + if self.isSleepTimerSet() { + // Update the UI NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) } } @@ -264,12 +265,6 @@ class AudioPlayer: NSObject { return } - // Capture remaining sleep time before changing the track position - let sleepSecondsRemaining = self.getSleepTimeRemaining() - - // Stop the paused timer - self.stopPausedTimer() - // Determine where we are starting playback let lastPlayed = (session.updatedAt ?? 0)/1000 let currentTime = allowSeekBack ? calculateSeekBackTimeAtCurrentTime(session.currentTime, lastPlayed: lastPlayed) : session.currentTime @@ -282,16 +277,7 @@ class AudioPlayer: NSObject { let seekTime = currentTime - currentTrackStartOffset self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] completed in guard completed else { return } - guard let self = self else { return } - - // Start playback - self.audioPlayer.play() - self.rate = self.tmpRate - self.audioPlayer.rate = self.tmpRate - self.status = 1 - - // Update the progress - self.updateNowPlaying() + self?.resumePlayback() } } @@ -320,9 +306,24 @@ class AudioPlayer: NSObject { return currentTime - time } + private func resumePlayback() { + NSLog("PLAY: Resuming playback") + + // Stop the paused timer + self.stopPausedTimer() + + self.audioPlayer.play() + self.audioPlayer.rate = self.tmpRate + self.status = 1 + + // Update the progress + self.updateNowPlaying() + } + public func pause() { guard self.isInitialized() else { return } + NSLog("PAUSE: Pausing playback") self.audioPlayer.pause() Task { @@ -332,7 +333,6 @@ class AudioPlayer: NSObject { } self.status = 0 - self.rate = 0.0 updateNowPlaying() @@ -342,10 +342,7 @@ class AudioPlayer: NSObject { public func seek(_ to: Double, from: String) { let continuePlaying = rate > 0.0 - // Capture remaining sleep time before changing the track position or pausing - let sleepSecondsRemaining = self.getSleepTimeRemaining() - - pause() + self.pause() NSLog("SEEK: Seek to \(to) from \(from)") @@ -386,22 +383,16 @@ class AudioPlayer: NSObject { guard completed else { return NSLog("SEEK: WARNING: seeking not completed (to \(seekTime)") } guard let self = self else { return } - // Reschedule the sleep timer - if let currentTime = self.getCurrentTime() { - self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining) + if continuePlaying { + self.resumePlayback() } - if continuePlaying { - self.play() - } self.updateNowPlaying() } } } public func setPlaybackRate(_ rate: Float, observed: Bool = false) { - // Capture remaining sleep time before changing the rate - let sleepSecondsRemaining = self.getSleepTimeRemaining() let playbackSpeedChanged = rate > 0.0 && rate != self.tmpRate && !(observed && rate == 1) if self.audioPlayer.rate != rate { @@ -415,11 +406,6 @@ class AudioPlayer: NSObject { if playbackSpeedChanged { self.tmpRate = rate - // If we have an active sleep timer, reschedule based on rate - if let currentTime = self.getCurrentTime() { - self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining) - } - // Setup the time observer again at the new rate self.setupTimeObserver() } diff --git a/ios/App/Shared/player/AudioPlayerSleepTimer.swift b/ios/App/Shared/player/AudioPlayerSleepTimer.swift index 85839984..96801559 100644 --- a/ios/App/Shared/player/AudioPlayerSleepTimer.swift +++ b/ios/App/Shared/player/AudioPlayerSleepTimer.swift @@ -12,54 +12,59 @@ extension AudioPlayer { // MARK: - Public API + public func isSleepTimerSet() -> Bool { + return self.isCountdownSleepTimerSet() || self.isChapterSleepTimerSet() + } + public func getSleepTimeRemaining() -> Double? { guard let currentTime = self.getCurrentTime() else { return nil } // Return the player time until sleep - var timeUntilSleep: Double? = nil + var sleepTimeRemaining: Double? = nil if let chapterStopAt = self.sleepTimeChapterStopAt { - timeUntilSleep = chapterStopAt - currentTime - } else if let stopAt = self.sleepTimeStopAt { - timeUntilSleep = stopAt - currentTime + sleepTimeRemaining = chapterStopAt - currentTime + } else if self.isCountdownSleepTimerSet() { + sleepTimeRemaining = self.sleepTimeRemaining } - // Scale the time until sleep based on the playback rate - if let timeUntilSleep = timeUntilSleep { - let timeUntilSleepScaled = timeUntilSleep / self.getPlaybackRate() - guard timeUntilSleepScaled.isNaN == false else { return nil } - - return timeUntilSleepScaled.rounded() - } else { + // Guard against invalid sleep timers + if sleepTimeRemaining?.isLess(than: 0) ?? false { + self.removeSleepTimer() return nil } + + return sleepTimeRemaining } - // Let iOS handle the sleep timer logic by letting us know when it's time to stop - public func setSleepTime(stopAt: Double, scaleBasedOnSpeed: Bool = false) { - NSLog("SLEEP TIMER: Scheduling for \(stopAt)") + public func setSleepTimer(secondsUntilSleep: Double) { + NSLog("SLEEP TIMER: Sleeping in \(secondsUntilSleep) seconds") + self.removeSleepTimer() + self.sleepTimeRemaining = secondsUntilSleep - // Reset any previous sleep timer - let isChapterSleepTimer = !scaleBasedOnSpeed - self.removeSleepTimer(resetStopAt: !isChapterSleepTimer) - - guard let currentTime = getCurrentTime() else { - NSLog("SLEEP TIMER: Failed to get currenTime") - return + DispatchQueue.runOnMainQueue { + self.sleepTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in + if self.isPlaying() { + self.decrementSleepTimerIfRunning() + } + } } - // Mark the time to stop playing - let scaledStopAt = self.calculateScaledStopAt(stopAt, currentTime: currentTime, scaleBasedOnSpeed: scaleBasedOnSpeed) - self.sleepTimeStopAt = scaledStopAt - let sleepTime = CMTime(seconds: scaledStopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + // Update the UI + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) + } + + public func setChapterSleepTimer(stopAt: Double) { + NSLog("SLEEP TIMER: Scheduling for chapter end \(stopAt)") + self.removeSleepTimer() // Schedule the observation time + self.sleepTimeChapterStopAt = stopAt + let sleepTime = CMTime(seconds: stopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) var times = [NSValue]() times.append(NSValue(time: sleepTime)) - self.sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: self.queue) { [weak self] in - NSLog("SLEEP TIMER: Pausing audio") - self?.pause() - self?.removeSleepTimer() + self.sleepTimeChapterToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: self.queue) { [weak self] in + self?.handleSleepEnd() } // Update the UI @@ -67,56 +72,66 @@ extension AudioPlayer { } public func increaseSleepTime(extraTimeInSeconds: Double) { - if let sleepTime = self.getSleepTimeRemaining(), let currentTime = getCurrentTime() { - let newSleepTimerPosition = currentTime + sleepTime + extraTimeInSeconds - if newSleepTimerPosition > currentTime { - self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) - } - } + self.removeChapterSleepTimer() + guard let sleepTimeRemaining = self.sleepTimeRemaining else { return } + self.sleepTimeRemaining = sleepTimeRemaining + extraTimeInSeconds + + // Update the UI + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) } public func decreaseSleepTime(removeTimeInSeconds: Double) { - if let sleepTime = self.getSleepTimeRemaining(), let currentTime = getCurrentTime() { - let newSleepTimerPosition = currentTime + sleepTime - removeTimeInSeconds - if newSleepTimerPosition > currentTime { - self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) - } - } - - } - - public func removeSleepTimer(resetStopAt: Bool = true) { - if resetStopAt { - self.sleepTimeStopAt = nil - self.sleepTimeChapterStopAt = nil - } - - if let token = self.sleepTimeToken { - self.audioPlayer.removeTimeObserver(token) - self.sleepTimeToken = nil - } + self.removeChapterSleepTimer() + guard let sleepTimeRemaining = self.sleepTimeRemaining else { return } + self.sleepTimeRemaining = sleepTimeRemaining - removeTimeInSeconds // Update the UI - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self) + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) + } + + public func removeSleepTimer() { + self.sleepTimer?.invalidate() + self.sleepTimer = nil + self.removeChapterSleepTimer() + self.sleepTimeRemaining = nil + + // Update the UI after a delay, to avoid a race condition when changing chapters + DispatchQueue.runOnMainQueue { + Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in + if !self.isSleepTimerSet() { + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self) + } + } + } } // MARK: - Internal helpers - internal func rescheduleSleepTimerAtTime(time: Double, secondsRemaining: Double?) { - guard self.isSleepTimerSet() else { return } - - // Cancel a chapter sleep timer that is no longer valid - if isChapterSleepTimerBeforeTime(time) { - return self.removeSleepTimer() + internal func decrementSleepTimerIfRunning() { + if var sleepTimeRemaining = self.sleepTimeRemaining { + sleepTimeRemaining -= 1 + self.sleepTimeRemaining = sleepTimeRemaining + + // Handle the sleep if the timer has expired + if sleepTimeRemaining <= 0 { + self.handleSleepEnd() + } } - - // Update the sleep timer - if !isChapterSleepTimer() { - guard let secondsRemaining = secondsRemaining else { return } - let newSleepTimerPosition = time + secondsRemaining - self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) + } + + private func handleSleepEnd() { + NSLog("SLEEP TIMER: Pausing audio") + self.pause() + self.removeSleepTimer() + } + + private func removeChapterSleepTimer() { + if let token = self.sleepTimeChapterToken { + self.audioPlayer.removeTimeObserver(token) } + self.sleepTimeChapterToken = nil + self.sleepTimeChapterStopAt = nil } private func isChapterSleepTimerBeforeTime(_ time: Double) -> Bool { @@ -127,29 +142,12 @@ extension AudioPlayer { return false } - private func isSleepTimerSet() -> Bool { - return self.sleepTimeStopAt != nil + private func isCountdownSleepTimerSet() -> Bool { + return self.sleepTimeRemaining != nil } - private func isChapterSleepTimer() -> Bool { + private func isChapterSleepTimerSet() -> Bool { return self.sleepTimeChapterStopAt != nil } - private func getPlaybackRate() -> Double { - // Consider paused as playing at 1x - return Double(self.rate > 0 ? self.rate : 1) - } - - private func calculateScaledStopAt(_ stopAt: Double, currentTime: Double, scaleBasedOnSpeed: Bool) -> Double { - if scaleBasedOnSpeed { - // Calculate the scaled time to stop at - let secondsUntilStopAt1x = stopAt - currentTime - let secondsUntilSleep = secondsUntilStopAt1x * self.getPlaybackRate() - NSLog("SLEEP TIMER: Adjusted based on playback speed of \(self.getPlaybackRate()) to \(secondsUntilSleep)") - return currentTime + secondsUntilSleep - } else { - return stopAt - } - } - } diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index a41414fb..975b5331 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -70,13 +70,11 @@ class PlayerHandler { } public static func setSleepTime(secondsUntilSleep: Double) { - guard let currentTime = self.player?.getCurrentTime() else { return } - let stopAt = secondsUntilSleep + currentTime - self.player?.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: true) + self.player?.setSleepTimer(secondsUntilSleep: secondsUntilSleep) } public static func setChapterSleepTime(stopAt: Double) { - self.player?.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: false) + self.player?.setChapterSleepTimer(stopAt: stopAt) } public static func increaseSleepTime(increaseSeconds: Double) { From c8ff5a7817e32377cfdfb54674dda1af4347a0f9 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 2 Sep 2022 18:31:16 -0400 Subject: [PATCH 61/66] Handle multi-track files for sleep timer --- ios/App/Shared/models/server/AudioTrack.swift | 7 +++++++ ios/App/Shared/player/AudioPlayer.swift | 2 +- .../Shared/player/AudioPlayerSleepTimer.swift | 19 +++++++++++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/ios/App/Shared/models/server/AudioTrack.swift b/ios/App/Shared/models/server/AudioTrack.swift index 8cd9e50e..f0e8f720 100644 --- a/ios/App/Shared/models/server/AudioTrack.swift +++ b/ios/App/Shared/models/server/AudioTrack.swift @@ -19,6 +19,13 @@ class AudioTrack: EmbeddedObject, Codable { @Persisted var localFileId: String? @Persisted var serverIndex: Int? + var endOffset: Double? { + if let startOffset = startOffset { + return startOffset + duration + } + return nil + } + private enum CodingKeys : String, CodingKey { case index, startOffset, duration, title, contentUrl, mimeType, metadata, localFileId, serverIndex } diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 78b66bd9..62c185f6 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -45,7 +45,7 @@ class AudioPlayer: NSObject { internal var sleepTimer: Timer? internal var sleepTimeRemaining: Double? - private var currentTrackIndex = 0 + internal var currentTrackIndex = 0 private var allPlayerItems:[AVPlayerItem] = [] private var pausedTimer: Timer? diff --git a/ios/App/Shared/player/AudioPlayerSleepTimer.swift b/ios/App/Shared/player/AudioPlayerSleepTimer.swift index 96801559..18ad7aee 100644 --- a/ios/App/Shared/player/AudioPlayerSleepTimer.swift +++ b/ios/App/Shared/player/AudioPlayerSleepTimer.swift @@ -22,7 +22,7 @@ extension AudioPlayer { // Return the player time until sleep var sleepTimeRemaining: Double? = nil if let chapterStopAt = self.sleepTimeChapterStopAt { - sleepTimeRemaining = chapterStopAt - currentTime + sleepTimeRemaining = (chapterStopAt - currentTime) / Double(self.rate > 0 ? self.rate : 1.0) } else if self.isCountdownSleepTimerSet() { sleepTimeRemaining = self.sleepTimeRemaining } @@ -59,7 +59,22 @@ extension AudioPlayer { // Schedule the observation time self.sleepTimeChapterStopAt = stopAt - let sleepTime = CMTime(seconds: stopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + + // Get the current track + guard let playbackSession = self.getPlaybackSession() else { return } + let currentTrack = playbackSession.audioTracks[currentTrackIndex] + + // Set values + guard let trackStartTime = currentTrack.startOffset else { return } + guard let trackEndTime = currentTrack.endOffset else { return } + + // Verify the stop is during the current audio track + guard trackEndTime >= stopAt else { return } + + // Schedule the observation time + let trackBasedStopTime = stopAt - trackStartTime + + let sleepTime = CMTime(seconds: trackBasedStopTime, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) var times = [NSValue]() times.append(NSValue(time: sleepTime)) From 836ffddd4fb81f0b5a115f483c13aa47dc43e008 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 2 Sep 2022 18:36:57 -0400 Subject: [PATCH 62/66] Fix race condition with chapter sleep timer --- ios/App/Shared/player/AudioPlayerSleepTimer.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayerSleepTimer.swift b/ios/App/Shared/player/AudioPlayerSleepTimer.swift index 18ad7aee..646ac5a6 100644 --- a/ios/App/Shared/player/AudioPlayerSleepTimer.swift +++ b/ios/App/Shared/player/AudioPlayerSleepTimer.swift @@ -27,8 +27,8 @@ extension AudioPlayer { sleepTimeRemaining = self.sleepTimeRemaining } - // Guard against invalid sleep timers - if sleepTimeRemaining?.isLess(than: 0) ?? false { + // Guard against invalid sleep timers, but give it a chance to go 1 second negative to prevent a raise condition + if sleepTimeRemaining?.isLess(than: -1) ?? false { self.removeSleepTimer() return nil } From fb8e6408bbde945f414a4caf5dc3fbfbb19f70e2 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Sat, 3 Sep 2022 16:34:31 -0400 Subject: [PATCH 63/66] Fix memory leak --- ios/App/Shared/player/AudioPlayer.swift | 47 +++++++++++-------- .../Shared/player/AudioPlayerSleepTimer.swift | 6 +-- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 62c185f6..6c22dc1c 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -100,20 +100,21 @@ class AudioPlayer: NSObject { NSLog("Audioplayer ready") } + deinit { self.stopPausedTimer() self.removeSleepTimer() self.removeTimeObserver() self.queueObserver?.invalidate() self.queueItemStatusObserver?.invalidate() - destroy() } + public func destroy() { // 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 // It is related to L79 `AVAudioSession.sharedInstance().setActive(false)` - pause() - audioPlayer.replaceCurrentItem(with: nil) + self.pause() + self.audioPlayer.replaceCurrentItem(with: nil) do { try AVAudioSession.sharedInstance().setActive(false) @@ -125,6 +126,11 @@ class AudioPlayer: NSObject { DispatchQueue.runOnMainQueue { UIApplication.shared.endReceivingRemoteControlEvents() } + + // Remove observers + self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.rate), context: &playerContext) + self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), context: &playerContext) + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil) } @@ -185,7 +191,8 @@ class AudioPlayer: NSObject { } private func setupQueueObserver() { - self.queueObserver = self.audioPlayer.observe(\.currentItem, options: [.new]) {_,_ in + self.queueObserver = self.audioPlayer.observe(\.currentItem, options: [.new]) { [weak self] _,_ in + guard let self = self else { return } let prevTrackIndex = self.currentTrackIndex self.audioPlayer.currentItem.map { item in self.currentTrackIndex = self.allPlayerItems.firstIndex(of:item) ?? 0 @@ -201,8 +208,8 @@ class AudioPlayer: NSObject { // Listen for player item updates self.queueItemStatusObserver?.invalidate() - self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { playerItem, change in - self.handleQueueItemStatus(playerItem: playerItem) + self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { [weak self] playerItem, change in + self?.handleQueueItemStatus(playerItem: playerItem) }) // Ensure we didn't miss a player item update during initialization @@ -486,59 +493,59 @@ class AudioPlayer: NSObject { let deviceSettings = Database.shared.getDeviceSettings() commandCenter.playCommand.isEnabled = true - commandCenter.playCommand.addTarget { [unowned self] event in - play(allowSeekBack: true) + commandCenter.playCommand.addTarget { [weak self] event in + self?.play(allowSeekBack: true) return .success } commandCenter.pauseCommand.isEnabled = true - commandCenter.pauseCommand.addTarget { [unowned self] event in - pause() + commandCenter.pauseCommand.addTarget { [weak self] event in + self?.pause() return .success } commandCenter.skipForwardCommand.isEnabled = true commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: deviceSettings.jumpForwardTime)] - commandCenter.skipForwardCommand.addTarget { [unowned self] event in + commandCenter.skipForwardCommand.addTarget { [weak self] event in guard let command = event.command as? MPSkipIntervalCommand else { return .noSuchContent } - guard let currentTime = self.getCurrentTime() else { + guard let currentTime = self?.getCurrentTime() else { return .commandFailed } - seek(currentTime + command.preferredIntervals[0].doubleValue, from: "remote") + self?.seek(currentTime + command.preferredIntervals[0].doubleValue, from: "remote") return .success } commandCenter.skipBackwardCommand.isEnabled = true commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: deviceSettings.jumpBackwardsTime)] - commandCenter.skipBackwardCommand.addTarget { [unowned self] event in + commandCenter.skipBackwardCommand.addTarget { [weak self] event in guard let command = event.command as? MPSkipIntervalCommand else { return .noSuchContent } - guard let currentTime = self.getCurrentTime() else { + guard let currentTime = self?.getCurrentTime() else { return .commandFailed } - seek(currentTime - command.preferredIntervals[0].doubleValue, from: "remote") + self?.seek(currentTime - command.preferredIntervals[0].doubleValue, from: "remote") return .success } commandCenter.changePlaybackPositionCommand.isEnabled = true - commandCenter.changePlaybackPositionCommand.addTarget { event in + commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in guard let event = event as? MPChangePlaybackPositionCommandEvent else { return .noSuchContent } - self.seek(event.positionTime, from: "remote") + 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 + commandCenter.changePlaybackRateCommand.addTarget { [weak self] event in guard let event = event as? MPChangePlaybackRateCommandEvent else { return .noSuchContent } - self.setPlaybackRate(event.playbackRate) + self?.setPlaybackRate(event.playbackRate) return .success } } diff --git a/ios/App/Shared/player/AudioPlayerSleepTimer.swift b/ios/App/Shared/player/AudioPlayerSleepTimer.swift index 646ac5a6..59511e94 100644 --- a/ios/App/Shared/player/AudioPlayerSleepTimer.swift +++ b/ios/App/Shared/player/AudioPlayerSleepTimer.swift @@ -42,9 +42,9 @@ extension AudioPlayer { self.sleepTimeRemaining = secondsUntilSleep DispatchQueue.runOnMainQueue { - self.sleepTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in - if self.isPlaying() { - self.decrementSleepTimerIfRunning() + self.sleepTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + if self?.isPlaying() ?? false { + self?.decrementSleepTimerIfRunning() } } } From b697deac1cfaafd98826a2cec5e4aca21cc4f7a2 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Sat, 3 Sep 2022 16:44:01 -0400 Subject: [PATCH 64/66] Fix cluegy way chapter sleep timer was being reset after skipping past --- ios/App/Shared/player/AudioPlayer.swift | 5 +++++ .../Shared/player/AudioPlayerSleepTimer.swift | 22 +++++-------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 6c22dc1c..f4199465 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -178,6 +178,11 @@ class AudioPlayer: NSObject { if self.isSleepTimerSet() { // Update the UI NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) + + // Handle a sitation where the user skips past the chapter end + if self.isChapterSleepTimerBeforeTime(currentTime) { + self.removeSleepTimer() + } } } } diff --git a/ios/App/Shared/player/AudioPlayerSleepTimer.swift b/ios/App/Shared/player/AudioPlayerSleepTimer.swift index 59511e94..b5995bc9 100644 --- a/ios/App/Shared/player/AudioPlayerSleepTimer.swift +++ b/ios/App/Shared/player/AudioPlayerSleepTimer.swift @@ -27,12 +27,6 @@ extension AudioPlayer { sleepTimeRemaining = self.sleepTimeRemaining } - // Guard against invalid sleep timers, but give it a chance to go 1 second negative to prevent a raise condition - if sleepTimeRemaining?.isLess(than: -1) ?? false { - self.removeSleepTimer() - return nil - } - return sleepTimeRemaining } @@ -110,14 +104,8 @@ extension AudioPlayer { self.removeChapterSleepTimer() self.sleepTimeRemaining = nil - // Update the UI after a delay, to avoid a race condition when changing chapters - DispatchQueue.runOnMainQueue { - Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in - if !self.isSleepTimerSet() { - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self) - } - } - } + // Update the UI + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self) } @@ -149,7 +137,7 @@ extension AudioPlayer { self.sleepTimeChapterStopAt = nil } - private func isChapterSleepTimerBeforeTime(_ time: Double) -> Bool { + internal func isChapterSleepTimerBeforeTime(_ time: Double) -> Bool { if let chapterStopAt = self.sleepTimeChapterStopAt { return chapterStopAt <= time } @@ -157,11 +145,11 @@ extension AudioPlayer { return false } - private func isCountdownSleepTimerSet() -> Bool { + internal func isCountdownSleepTimerSet() -> Bool { return self.sleepTimeRemaining != nil } - private func isChapterSleepTimerSet() -> Bool { + internal func isChapterSleepTimerSet() -> Bool { return self.sleepTimeChapterStopAt != nil } From 41753cd853b8467139890ca68e231d14ef867bd3 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Sat, 3 Sep 2022 17:18:54 -0400 Subject: [PATCH 65/66] Fix marking as read crashing app --- ios/App/Shared/models/local/LocalMediaProgress.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ios/App/Shared/models/local/LocalMediaProgress.swift b/ios/App/Shared/models/local/LocalMediaProgress.swift index c034ae8d..fc93198b 100644 --- a/ios/App/Shared/models/local/LocalMediaProgress.swift +++ b/ios/App/Shared/models/local/LocalMediaProgress.swift @@ -157,7 +157,19 @@ extension LocalMediaProgress { } } + static func getLocalMediaProgressId(localLibraryItemId: String?, localEpisodeId: String?) -> String? { + if let itemId = localLibraryItemId, let episodeId = localEpisodeId { + return "\(itemId)-\(episodeId)" + } else if let itemId = localLibraryItemId { + return itemId + } else { + return nil + } + } + static func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) throws -> LocalMediaProgress? { + let localMediaProgressId = localMediaProgressId != nil ? localMediaProgressId : LocalMediaProgress.getLocalMediaProgressId(localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) + let realm = try Realm() return try realm.write { () -> LocalMediaProgress? in if let localMediaProgressId = localMediaProgressId { From 9821ddabcfded760cf5a39b0a569c23778c86741 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 3 Sep 2022 16:42:14 -0500 Subject: [PATCH 66/66] iOS version bump 0.9.58-beta --- ios/App/App.xcodeproj/project.pbxproj | 8 ++++---- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 1f223fbf..3c92a7ed 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -599,12 +599,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Icons; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_TEAM = 7UFJ7D8V6A; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.9.57; + MARKETING_VERSION = 0.9.58; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -623,12 +623,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Icons; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_TEAM = 7UFJ7D8V6A; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.9.57; + MARKETING_VERSION = 0.9.58; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/package-lock.json b/package-lock.json index fdd727b9..0fcc0323 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-app", - "version": "0.9.57-beta", + "version": "0.9.58-beta", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index d06284b3..d6949401 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-app", - "version": "0.9.57-beta", + "version": "0.9.58-beta", "author": "advplyr", "scripts": { "dev": "nuxt --hostname 0.0.0.0 --port 1337",