Realm modifications should occur on concurrent queue

This commit is contained in:
ronaldheft
2022-08-23 22:37:28 -04:00
parent 452b25057e
commit 46623d70a3
2 changed files with 80 additions and 72 deletions
+3 -5
View File
@@ -18,8 +18,6 @@ enum PlayMethod:Int {
} }
class AudioPlayer: NSObject { class AudioPlayer: NSObject {
private let audioPlayerQueue = DispatchQueue(label: "ABSAudioPlayerQueue")
// enums and @objc are not compatible // enums and @objc are not compatible
@objc dynamic var status: Int @objc dynamic var status: Int
@objc dynamic var rate: Float @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 // 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 seconds = 0.5 * (self.rate > 0 ? self.rate : 1.0)
let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale) 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 let sleepTimeStopAt = self?.sleepTimeStopAt
Task { Task {
// Let the player update the current playback positions // Let the player update the current playback positions
@@ -207,7 +205,7 @@ class AudioPlayer: NSObject {
private func startPausedTimer() { private func startPausedTimer() {
guard self.pausedTimer == nil else { return } guard self.pausedTimer == nil else { return }
audioPlayerQueue.async { PlayerProgress.queue.async {
self.pausedTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in self.pausedTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in
NSLog("PAUSE TIMER: Syncing from server") NSLog("PAUSE TIMER: Syncing from server")
Task { await PlayerProgress.shared.syncFromServer() } Task { await PlayerProgress.shared.syncFromServer() }
@@ -400,7 +398,7 @@ class AudioPlayer: NSObject {
var times = [NSValue]() var times = [NSValue]()
times.append(NSValue(time: sleepTime)) 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") NSLog("SLEEP TIMER: Pausing audio")
self?.pause() self?.pause()
self?.removeSleepTimer() self?.removeSleepTimer()
+71 -61
View File
@@ -10,8 +10,8 @@ import UIKit
import RealmSwift import RealmSwift
class PlayerProgress { class PlayerProgress {
public static let shared = PlayerProgress() public static let shared = PlayerProgress()
public static let queue = DispatchQueue(label: "ABSPlayerProgressQueue")
private static let TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 10.0 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 { public func syncFromPlayer(currentTime: Double, includesPlayProgress: Bool, isStopping: Bool) async {
let backgroundToken = await UIApplication.shared.beginBackgroundTask(withName: "ABS:syncFromPlayer") 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() updateLocalMediaProgressFromLocalSession()
if let session = session { if let session = session {
await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping) await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping)
@@ -45,48 +45,52 @@ class PlayerProgress {
// MARK: - SYNC LOGIC // MARK: - SYNC LOGIC
private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) async -> PlaybackSession? { private func updateLocalSessionFromPlayer(currentTime: Double, includesPlayProgress: Bool) -> PlaybackSession? {
guard let session = PlayerHandler.getPlaybackSession() else { return nil } PlayerProgress.queue.sync {
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.update {
session.realm?.refresh() session.realm?.refresh()
let nowInSeconds = Date().timeIntervalSince1970 let nowInSeconds = Date().timeIntervalSince1970
let nowInMilliseconds = nowInSeconds * 1000 let nowInMilliseconds = nowInSeconds * 1000
let lastUpdateInMilliseconds = session.updatedAt ?? nowInMilliseconds let lastUpdateInMilliseconds = session.updatedAt ?? nowInMilliseconds
let lastUpdateInSeconds = lastUpdateInMilliseconds / 1000 let lastUpdateInSeconds = lastUpdateInMilliseconds / 1000
let secondsSinceLastUpdate = nowInSeconds - lastUpdateInSeconds let secondsSinceLastUpdate = nowInSeconds - lastUpdateInSeconds
session.currentTime = currentTime session.currentTime = currentTime
session.updatedAt = nowInMilliseconds session.updatedAt = nowInMilliseconds
if includesPlayProgress { if includesPlayProgress {
session.timeListening += secondsSinceLastUpdate session.timeListening += secondsSinceLastUpdate
}
} }
}
return session.freeze() return session.freeze()
}
} }
private func updateLocalMediaProgressFromLocalSession() { private func updateLocalMediaProgressFromLocalSession() {
guard let session = PlayerHandler.getPlaybackSession() else { return } PlayerProgress.queue.sync {
guard session.isLocal else { return } 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 = LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: session.localMediaProgressId, localLibraryItemId: session.localLibraryItem?.id, localEpisodeId: session.episodeId)
guard let localMediaProgress = localMediaProgress else { guard let localMediaProgress = localMediaProgress else {
// Local media progress should have been created // Local media progress should have been created
// If we're here, it means a library id is invalid // If we're here, it means a library id is invalid
return 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 { private func updateAllServerSessionFromLocalSession() async {
@@ -102,31 +106,32 @@ class PlayerProgress {
} }
private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async { private func updateServerSessionFromLocalSession(_ session: PlaybackSession, rateLimitSync: Bool = false) async {
guard var session = session.thaw() else { return } PlayerProgress.queue.sync {
var safeToSync = true 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 // We need to update and check the server time in a transaction for thread-safety
session.update { session.update {
session.realm?.refresh() session.realm?.refresh()
let nowInMilliseconds = Date().timeIntervalSince1970 * 1000 let nowInMilliseconds = Date().timeIntervalSince1970 * 1000
let lastUpdateInMilliseconds = session.serverUpdatedAt let lastUpdateInMilliseconds = session.serverUpdatedAt
// If required, rate limit requests based on session last update // If required, rate limit requests based on session last update
if rateLimitSync { if rateLimitSync {
let timeSinceLastSync = nowInMilliseconds - lastUpdateInMilliseconds let timeSinceLastSync = nowInMilliseconds - lastUpdateInMilliseconds
let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000 let timeBetweenSessionSync = PlayerProgress.TIME_BETWEEN_SESSION_SYNC_IN_SECONDS * 1000
safeToSync = timeSinceLastSync > timeBetweenSessionSync safeToSync = timeSinceLastSync > timeBetweenSessionSync
if !safeToSync { if !safeToSync {
return // This only exits the update block return // This only exits the update block
}
} }
session.serverUpdatedAt = nowInMilliseconds
} }
session = session.freeze()
session.serverUpdatedAt = nowInMilliseconds guard safeToSync else { return }
} }
session = session.freeze()
guard safeToSync else { return }
NSLog("Sending sessionId(\(session.id)) to server") NSLog("Sending sessionId(\(session.id)) to server")
@@ -138,10 +143,13 @@ class PlayerProgress {
success = await ApiClient.reportPlaybackProgress(report: playbackReport, sessionId: session.id) success = await ApiClient.reportPlaybackProgress(report: playbackReport, sessionId: session.id)
} }
// Remove old sessions after they synced with the server // Remove old sessions after they synced with the server
if success && !session.isActiveSession { if success && !session.isActiveSession {
if let session = session.thaw() { PlayerProgress.queue.sync {
session.delete() if let session = session.thaw() {
session.delete()
}
} }
} }
} }
@@ -176,14 +184,16 @@ class PlayerProgress {
// Update the session, if needed // Update the session, if needed
if serverIsNewerThanLocal && currentTimeIsDifferent { if serverIsNewerThanLocal && currentTimeIsDifferent {
NSLog("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)") PlayerProgress.queue.sync {
guard let session = session.thaw() else { return } NSLog("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)")
session.update { guard let session = session.thaw() else { return }
session.currentTime = serverCurrentTime session.update {
session.updatedAt = serverLastUpdate 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 { } else {
NSLog("updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress") NSLog("updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress")
} }