Skip to content

Commit

Permalink
Merge pull request #1058 from TortugaPower/fix-lastplayed-progress
Browse files Browse the repository at this point in the history
Fix local playback overriding progress while fetching remote progress
  • Loading branch information
GianniCarlo authored Dec 18, 2023
2 parents 5290438 + 08d7983 commit 37ebabf
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 51 deletions.
12 changes: 12 additions & 0 deletions BookPlayer/Coordinators/FolderListCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ class FolderListCoordinator: ItemListCoordinator {
}

override func syncList() {
let userDefaultsKey = "\(Constants.UserDefaults.lastSyncTimestamp)_\(folderRelativePath)"
let now = Date().timeIntervalSince1970
let lastSync = UserDefaults.standard.double(forKey: userDefaultsKey)

/// Do not sync if one minute hasn't passed since last sync
guard now - lastSync > 60 else {
Self.logger.trace("Throttled sync operation")
return
}

UserDefaults.standard.set(now, forKey: userDefaultsKey)

Task { @MainActor in
do {
_ = try await syncService.syncListContents(at: folderRelativePath)
Expand Down
24 changes: 23 additions & 1 deletion BookPlayer/Coordinators/LibraryListCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat
var importOperationSubscription: AnyCancellable?
/// Reference to know if the import screen is already being shown (or in the process of showing)
weak var importCoordinator: ImportCoordinator?
/// Reference to ongoing library fetch task
var contentsFetchTask: Task<(), Error>?

private var disposeBag = Set<AnyCancellable>()

Expand Down Expand Up @@ -212,7 +214,21 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat
}

override func syncList() {
Task { @MainActor in
let userDefaultsKey = "\(Constants.UserDefaults.lastSyncTimestamp)_library"
let now = Date().timeIntervalSince1970
let lastSync = UserDefaults.standard.double(forKey: userDefaultsKey)

/// Do not sync if one minute hasn't passed since last sync
guard now - lastSync > 60 else {
Self.logger.trace("Throttled sync operation")
return
}

UserDefaults.standard.set(now, forKey: userDefaultsKey)

/// Create new task to sync the library and the last played
contentsFetchTask?.cancel()
contentsFetchTask = Task { @MainActor in
do {
if UserDefaults.standard.bool(forKey: Constants.UserDefaults.hasScheduledLibraryContents) == true {
try await syncService.syncListContents(at: nil)
Expand Down Expand Up @@ -267,3 +283,9 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat
)
}
}

extension LibraryListCoordinator: PlaybackSyncProgressDelegate {
func waitForSyncInProgress() async {
_ = await contentsFetchTask?.result
}
}
1 change: 1 addition & 0 deletions BookPlayer/Coordinators/MainCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class MainCoordinator: NSObject {
playbackService: self.playbackService,
syncService: syncService
)
playerManager.syncProgressDelegate = libraryCoordinator
self.libraryCoordinator = libraryCoordinator
libraryCoordinator.tabBarController = tabBarController
libraryCoordinator.start()
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/Generated/AutoMockable.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,7 @@ class PlayerManagerProtocolMock: PlayerManagerProtocol {
set(value) { underlyingIsPlaying = value }
}
var underlyingIsPlaying: Bool!
var syncProgressDelegate: PlaybackSyncProgressDelegate?
//MARK: - load

var loadAutoplayCallsCount = 0
Expand Down
98 changes: 63 additions & 35 deletions BookPlayer/Player/PlayerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import MediaPlayer

// swiftlint:disable:next file_length
/// sourcery: AutoMockable
public protocol PlayerManagerProtocol {
public protocol PlayerManagerProtocol: AnyObject {
var currentItem: PlayableItem? { get set }
var currentSpeed: Float { get set }
var isPlaying: Bool { get }
var syncProgressDelegate: PlaybackSyncProgressDelegate? { get set }

func load(_ item: PlayableItem, autoplay: Bool)
func hasLoadedBook() -> Bool
Expand All @@ -41,6 +42,11 @@ public protocol PlayerManagerProtocol {
func currentItemPublisher() -> AnyPublisher<PlayableItem?, Never>
}

/// Delegate that hooks into the playback sequence
public protocol PlaybackSyncProgressDelegate: AnyObject {
func waitForSyncInProgress() async
}

final class PlayerManager: NSObject, PlayerManagerProtocol {
private let libraryService: LibraryServiceProtocol
private let playbackService: PlaybackServiceProtocol
Expand Down Expand Up @@ -81,6 +87,9 @@ final class PlayerManager: NSObject, PlayerManagerProtocol {
}
}

weak var syncProgressDelegate: PlaybackSyncProgressDelegate?
/// Reference to the ongoing play task
private var playTask: Task<(), Error>?
private var playerItem: AVPlayerItem?
private var loadChapterTask: Task<(), Never>?
@Published var currentItem: PlayableItem?
Expand Down Expand Up @@ -274,6 +283,7 @@ final class PlayerManager: NSObject, PlayerManagerProtocol {

private func load(_ item: PlayableItem, autoplay: Bool, forceRefreshURL: Bool) {
/// Cancel in case there's an ongoing load task
playTask?.cancel()
loadChapterTask?.cancel()

// Recover in case of failure
Expand Down Expand Up @@ -653,10 +663,7 @@ extension PlayerManager {
// MARK: - Playback

extension PlayerManager {
func play() {
/// Ignore play commands if there's no item loaded
guard let currentItem else { return }

func prepareForPlayback(_ currentItem: PlayableItem) async -> Bool {
/// Allow refetching remote URL if the action was initiating by the user
canFetchRemoteURL = true

Expand All @@ -669,7 +676,7 @@ extension PlayerManager {
load(currentItem, autoplay: true)
}
}
return
return false
}

guard playerItem.status == .readyToPlay && playerItem.error == nil else {
Expand All @@ -682,44 +689,63 @@ extension PlayerManager {
self.observeStatus = true
}

return
return false
}

self.userActivityManager.resumePlaybackActivity()
/// Update nowPlaying state so the UI displays correctly
playbackQueued = true
await syncProgressDelegate?.waitForSyncInProgress()

do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(
AVAudioSession.Category.playback,
mode: .spokenAudio,
options: []
)
try audioSession.setActive(true)
} catch {
fatalError("Failed to activate the audio session, \(error), description: \(error.localizedDescription)")
}
return true
}

self.createOrUpdateAutomaticBookmark(
at: currentItem.currentTime,
relativePath: currentItem.relativePath,
type: .play
)
func play() {
playTask?.cancel()
playTask = Task { @MainActor in
/// Ignore play commands if there's no item loaded,
/// and only continue if the item is loaded and ready
guard
let currentItem,
await prepareForPlayback(currentItem),
!Task.isCancelled
else { return }

userActivityManager.resumePlaybackActivity()

do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(
AVAudioSession.Category.playback,
mode: .spokenAudio,
options: []
)
try audioSession.setActive(true)
} catch {
fatalError("Failed to activate the audio session, \(error), description: \(error.localizedDescription)")
}

createOrUpdateAutomaticBookmark(
at: currentItem.currentTime,
relativePath: currentItem.relativePath,
type: .play
)

// If book is completed, stop
if Int(currentItem.duration) == Int(CMTimeGetSeconds(self.audioPlayer.currentTime())) { return }
// If book is completed, stop
if Int(currentItem.duration) == Int(CMTimeGetSeconds(audioPlayer.currentTime())) { return }

self.handleSmartRewind(currentItem)
handleSmartRewind(currentItem)

self.fadeTimer?.invalidate()
self.shakeMotionService.stopMotionUpdates()
self.boostVolume = UserDefaults.standard.bool(forKey: Constants.UserDefaults.boostVolumeEnabled)
bindInterruptObserver()
// Set play state on player and control center
self.audioPlayer.playImmediately(atRate: self.currentSpeed)
fadeTimer?.invalidate()
shakeMotionService.stopMotionUpdates()
boostVolume = UserDefaults.standard.bool(forKey: Constants.UserDefaults.boostVolumeEnabled)
bindInterruptObserver()
// Set play state on player and control center
audioPlayer.playImmediately(atRate: currentSpeed)
/// Clean up flag after player starts playing
playbackQueued = nil

self.setNowPlayingBookTitle(chapter: currentItem.currentChapter)
setNowPlayingBookTitle(chapter: currentItem.currentChapter)

DispatchQueue.main.async {
NotificationCenter.default.post(name: .bookPlayed, object: nil, userInfo: ["book": currentItem])
}
}
Expand Down Expand Up @@ -819,6 +845,7 @@ extension PlayerManager {
// Set pause state on player and control center
audioPlayer.pause()
playbackQueued = nil
playTask?.cancel()
loadChapterTask?.cancel()
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
MPNowPlayingInfoCenter.default().playbackState = .paused
Expand Down Expand Up @@ -858,6 +885,7 @@ extension PlayerManager {
playbackQueued = nil

audioPlayer.pause()
playTask?.cancel()
loadChapterTask?.cancel()

userActivityManager.stopPlaybackActivity()
Expand Down
15 changes: 0 additions & 15 deletions Shared/Services/Sync/SyncService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import Combine
import Foundation
import RevenueCat

/// Sync errors that must be handled (not shown as alerts)
public enum BPSyncError: Error {
Expand Down Expand Up @@ -164,20 +163,6 @@ public final class SyncService: SyncServiceProtocol, BPLogger {
throw BookPlayerError.runtimeError("Can't fetch items while there are sync operations in progress")
}

let userDefaultsKey = "\(Constants.UserDefaults.lastSyncTimestamp)_\(relativePath ?? "library")"
let now = Date().timeIntervalSince1970
let lastSync = UserDefaults.standard.double(forKey: userDefaultsKey)

/// Do not sync if one minute hasn't passed since last sync
guard now - lastSync > 60 else {
throw BookPlayerError.networkError("Throttled sync operation")
}

UserDefaults.standard.set(
Date().timeIntervalSince1970,
forKey: userDefaultsKey
)

Self.logger.trace("Fetching list of contents")

let response = try await fetchContents(at: relativePath)
Expand Down

0 comments on commit 37ebabf

Please sign in to comment.