From 6257c6488ba69412e7f7841f54e8d9c3ce0a4ac7 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Wed, 31 Aug 2022 23:15:25 -0400 Subject: [PATCH 1/4] Fix race condition with the player queue --- ios/App/Shared/player/AudioPlayer.swift | 66 +++++++++++++++---------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index bc69f9fb..5bb5e962 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -195,36 +195,48 @@ class AudioPlayer: NSObject { private func setupQueueItemStatusObserver() { self.queueItemStatusObserver?.invalidate() - self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in - guard let playbackSession = self.getPlaybackSession() else { - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) - return - } - if (playerItem.status == .readyToPlay) { - NSLog("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)") - self.updateNowPlaying() - - // Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session - let firstReady = self.status < 0 - if firstReady || self.playWhenReady { - self.seek(playbackSession.currentTime, from: "queueItemStatusObserver") - } - - // Mark the player as ready - self.status = 0 - - // Start the player, if requested - if self.playWhenReady { - self.playWhenReady = false - self.play() - } - } else if (playerItem.status == .failed) { - NSLog("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")") - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) - } + let status = self.audioPlayer.currentItem?.status.rawValue ?? -1 + + NSLog("queueStatusObserver: Setting up status=\(status)") + // First item already loaded, we need to fire manually + if status == 1, let playerItem = self.audioPlayer.currentItem { + self.handleQueueItemStatus(playerItem: playerItem) + } + // Now listen for future updates + self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { playerItem, change in + self.handleQueueItemStatus(playerItem: playerItem) }) } + private func handleQueueItemStatus(playerItem: AVPlayerItem) { + NSLog("queueStatusObserver: Current item status changed") + guard let playbackSession = self.getPlaybackSession() else { + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) + return + } + if (playerItem.status == .readyToPlay) { + NSLog("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)") + + // Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session + let firstReady = self.status < 0 + if firstReady || self.playWhenReady { + self.seek(playbackSession.currentTime, from: "queueItemStatusObserver") + } + + // Mark the player as ready + self.status = 0 + + // Start the player, if requested + if self.playWhenReady { + self.playWhenReady = false + self.play() + } + } else if (playerItem.status == .failed) { + NSLog("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")") + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) + } + } + private func startPausedTimer() { guard self.pausedTimer == nil else { return } self.queue.async { From 50f6b15d233336d992d7f6f14c9b1bc5e21106bc Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 1 Sep 2022 17:40:49 -0400 Subject: [PATCH 2/4] Refactor to ensure we aren't missing any update time --- ios/App/Shared/player/AudioPlayer.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 5bb5e962..b20d69d7 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -194,18 +194,18 @@ class AudioPlayer: NSObject { } private func setupQueueItemStatusObserver() { + NSLog("queueStatusObserver: Setting up") + + // Listen for player item updates self.queueItemStatusObserver?.invalidate() - let status = self.audioPlayer.currentItem?.status.rawValue ?? -1 - - NSLog("queueStatusObserver: Setting up status=\(status)") - // First item already loaded, we need to fire manually - if status == 1, let playerItem = self.audioPlayer.currentItem { - self.handleQueueItemStatus(playerItem: playerItem) - } - // Now listen for future updates self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { playerItem, change in self.handleQueueItemStatus(playerItem: playerItem) }) + + // Ensure we didn't miss a player item update during initialization + if let playerItem = self.audioPlayer.currentItem { + self.handleQueueItemStatus(playerItem: playerItem) + } } private func handleQueueItemStatus(playerItem: AVPlayerItem) { From 021d63d973f2382996b1d5af14df1c4468018050 Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 1 Sep 2022 18:19:45 -0400 Subject: [PATCH 3/4] Fix seek jumping --- ios/App/Shared/player/AudioPlayer.swift | 96 ++++++++++++++----------- 1 file changed, 53 insertions(+), 43 deletions(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index b20d69d7..3341d4f1 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -219,7 +219,9 @@ class AudioPlayer: NSObject { // Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session let firstReady = self.status < 0 - if firstReady || self.playWhenReady { + if firstReady || !self.playWhenReady { + // Seek is async, and if we call this when also pressing play, we will get weird jumps in the scrub bar depending on timing + // Seeking to the correct position happens during play() self.seek(playbackSession.currentTime, from: "queueItemStatusObserver") } @@ -255,59 +257,67 @@ class AudioPlayer: NSObject { // MARK: - Methods public func play(allowSeekBack: Bool = false) { guard self.isInitialized() else { return } + guard let session = self.getPlaybackSession() else { + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) + return + } // Capture remaining sleep time before changing the track position let sleepSecondsRemaining = PlayerHandler.remainingSleepTime - if allowSeekBack, let session = self.getPlaybackSession() { - let lastPlayed = (session.updatedAt ?? 0)/1000 - let difference = Date.timeIntervalSinceReferenceDate - lastPlayed - var time: Int? - - if lastPlayed == 0 { - time = 5 - } else if difference < 6 { - time = 2 - } else if difference < 12 { - time = 10 - } else if difference < 30 { - time = 15 - } else if difference < 180 { - time = 20 - } else if difference < 3600 { - time = 25 - } else { - time = 29 - } - - if time != nil { - guard let currentTime = self.getCurrentTime() else { - NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) - return - } - seek(currentTime - Double(time!), from: "play") - } - } - + // Stop the paused timer self.stopPausedTimer() - Task { - if let currentTime = self.getCurrentTime() { - await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: self.isPlaying(), isStopping: false) + // Determine where we are starting playback + let lastPlayed = (session.updatedAt ?? 0)/1000 + let currentTime = allowSeekBack ? calculateSeekBackTimeAtCurrentTime(session.currentTime, lastPlayed: lastPlayed) : session.currentTime + + // Sync our new playback position + Task { await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: self.isPlaying(), isStopping: false) } + + // Start playback, with a seek, for as smooth a scrub bar start as possible + let currentTrackStartOffset = session.audioTracks[self.currentTrackIndex].startOffset ?? 0.0 + let seekTime = currentTime - currentTrackStartOffset + self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] completed in + if completed, let self = self { + // Start playback + self.audioPlayer.play() + self.rate = self.tmpRate + self.audioPlayer.rate = self.tmpRate + self.status = 1 + + // If we have an active sleep timer, reschedule based on rate + self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining) + + // Update the progress + self.updateNowPlaying() } } - - self.audioPlayer.play() - self.status = 1 - self.rate = self.tmpRate - self.audioPlayer.rate = self.tmpRate + } + + private func calculateSeekBackTimeAtCurrentTime(_ currentTime: Double, lastPlayed: Double) -> Double { + let difference = Date.timeIntervalSinceReferenceDate - lastPlayed + var time: Double = 0 - // If we have an active sleep timer, reschedule based on rate - if let currentTime = self.getCurrentTime() { - self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining) + // Scale seek back time based on how long since last play + if lastPlayed == 0 { + time = 5 + } else if difference < 6 { + time = 2 + } else if difference < 12 { + time = 10 + } else if difference < 30 { + time = 15 + } else if difference < 180 { + time = 20 + } else if difference < 3600 { + time = 25 + } else { + time = 29 } - updateNowPlaying() + // Wind the clock back + return currentTime - time } public func pause() { From c02adfa32615ce0cf62aa4fbea4c79bff686babe Mon Sep 17 00:00:00 2001 From: ronaldheft Date: Thu, 1 Sep 2022 18:41:21 -0400 Subject: [PATCH 4/4] Fix typo in if statement --- ios/App/Shared/player/AudioPlayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 3341d4f1..affc56b2 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -219,7 +219,7 @@ class AudioPlayer: NSObject { // Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session let firstReady = self.status < 0 - if firstReady || !self.playWhenReady { + if firstReady && !self.playWhenReady { // Seek is async, and if we call this when also pressing play, we will get weird jumps in the scrub bar depending on timing // Seeking to the correct position happens during play() self.seek(playbackSession.currentTime, from: "queueItemStatusObserver")