From 27d2ed230446219b370c7ff523bf18992ee3730e Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Sun, 21 Aug 2022 12:06:37 -0400 Subject: [PATCH 01/36] 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/36] 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 d57fe44bcc8b73ee50cb9d855c9164c80213c503 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 22 Aug 2022 17:04:48 -0400 Subject: [PATCH 03/36] 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 04/36] 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 05/36] 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 06/36] 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 2448b461f09344da1d6e116effaf4a8e79f69293 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 22 Aug 2022 18:00:37 -0400 Subject: [PATCH 07/36] 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 08/36] 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 09/36] 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 15cdff5aa2140e8056c9694c0b06b1f59275b5bc Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Mon, 22 Aug 2022 20:36:15 -0400 Subject: [PATCH 10/36] 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 11/36] 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 12/36] 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 13/36] 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 14/36] 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 15/36] 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 16/36] 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 17/36] 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 18/36] 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 b5e33b1707c5cc8021505175ae4200ffd20e3250 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Wed, 24 Aug 2022 19:33:10 -0400 Subject: [PATCH 19/36] 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 20/36] 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 21/36] 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 22/36] 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 23/36] 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 24/36] 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 25/36] 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 26/36] 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 27/36] 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 eb7a241e944d812be6f562a4d6033611d2f1c148 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 25 Aug 2022 19:03:05 -0400 Subject: [PATCH 28/36] 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 2076b93e1969f10059cb920c6fca9fc656009b1b Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Fri, 26 Aug 2022 18:35:47 -0400 Subject: [PATCH 29/36] 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 30/36] 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 31/36] 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 32/36] 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 33/36] 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 34/36] 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 35/36] 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 36/36] 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