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") }