Skip to content

Commit

Permalink
Merge pull request #950 from TortugaPower/sync-refresh-remoteurl
Browse files Browse the repository at this point in the history
Refresh remote URL when it expires for playback streaming
  • Loading branch information
GianniCarlo authored Jul 1, 2023
2 parents a8990e8 + 6115434 commit 281a313
Show file tree
Hide file tree
Showing 16 changed files with 346 additions and 195 deletions.
5 changes: 4 additions & 1 deletion BookPlayer/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
if let sharedSyncService = AppDelegate.shared?.syncService {
syncService = sharedSyncService
} else {
syncService = SyncService(libraryService: libraryService)
syncService = SyncService(
isActive: accountService.hasActiveSubscription(),
libraryService: libraryService
)
AppDelegate.shared?.syncService = syncService
}

Expand Down
6 changes: 2 additions & 4 deletions BookPlayer/Coordinators/FolderListCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,8 @@ class FolderListCoordinator: ItemListCoordinator {
self?.showItemSelectionScreen(availableItems: availableItems, selectionHandler: selectionHandler)
case .showMiniPlayer(let flag):
self?.showMiniPlayer(flag: flag)
case .bindImportObservers:
/// Only the library list coordinator handle these events
break
case .listDidAppear:
self?.syncList()
}
}
viewModel.coordinator = self
Expand All @@ -71,7 +70,6 @@ class FolderListCoordinator: ItemListCoordinator {
navigationController.pushViewController(vc, animated: true)

documentPickerDelegate = vc
syncList()
}

override func syncList() {
Expand Down
15 changes: 9 additions & 6 deletions BookPlayer/Coordinators/LibraryListCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ class LibraryListCoordinator: ItemListCoordinator {
self?.showItemSelectionScreen(availableItems: availableItems, selectionHandler: selectionHandler)
case .showMiniPlayer(let flag):
self?.showMiniPlayer(flag: flag)
case .bindImportObservers:
self?.bindImportObserverIfNeeded()
case .listDidAppear:
self?.handleLibraryLoaded()
}
}
viewModel.coordinator = self
Expand All @@ -89,8 +89,6 @@ class LibraryListCoordinator: ItemListCoordinator {
tabBarController.setViewControllers(newControllersArray, animated: false)
}

self.loadLastBookIfNeeded()

if let appDelegate = AppDelegate.shared {
for action in appDelegate.pendingURLActions {
ActionParserService.handleAction(action)
Expand All @@ -100,7 +98,12 @@ class LibraryListCoordinator: ItemListCoordinator {
self.documentPickerDelegate = vc

AppDelegate.shared?.watchConnectivityService?.startSession()
}

func handleLibraryLoaded() {
loadLastBookIfNeeded()
syncList()
bindImportObserverIfNeeded()
}

func bindImportObserverIfNeeded() {
Expand Down Expand Up @@ -236,8 +239,8 @@ class LibraryListCoordinator: ItemListCoordinator {

/// Check to update current time or not
if item.relativePath == localLastItem.relativePath {
/// Add a padding of 5 mins to local time
if item.currentTime > (localLastItem.currentTime + 300) {
/// Add a padding of 1 min to local time
if item.currentTime > (localLastItem.currentTime + 60) {
/// Continue playback after time sync
let wasPlaying = playerManager.isPlaying
playerManager.stop()
Expand Down
10 changes: 3 additions & 7 deletions BookPlayer/Coordinators/MainCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ class MainCoordinator: Coordinator {

super.init(navigationController: navigationController, flowType: .modal)

accountService.setDelegate(self)
setUpTheming()
}

Expand Down Expand Up @@ -70,7 +69,7 @@ class MainCoordinator: Coordinator {

bindObservers()

accountService.loginIfUserExists()
accountService.loginIfUserExists(delegate: self)

startLibraryCoordinator(with: tabBarController)

Expand Down Expand Up @@ -134,19 +133,16 @@ class MainCoordinator: Coordinator {
/** Disable socket lifecycle events
self.socketService.connectSocket()
*/
let libraryCoordinator = self.getLibraryCoordinator()

if !self.syncService.isActive {
self.syncService.isActive = true
libraryCoordinator?.syncLibrary()
} else if !self.playerManager.hasLoadedBook() {
libraryCoordinator?.loadLastBookIfNeeded()
self.getLibraryCoordinator()?.syncLibrary()
}
} else {
/** Disable socket lifecycle events
self.socketService.disconnectSocket()
*/
self.syncService.isActive = false
self.syncService.cancelAllJobs()
}

})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class ItemListViewController: BaseViewController<ItemListCoordinator, ItemListVi

@IBOutlet weak var tableView: UITableView!

/// This is required to know if the initial Library layout is presented, and other screens can be presented on top
private var didAppearForFirstTime = true

private lazy var searchButton: UIBarButtonItem = {
return UIBarButtonItem(systemItem: .search, primaryAction: UIAction { [weak self] _ in
self?.navigationItem.backButtonDisplayMode = .minimal
Expand Down Expand Up @@ -98,7 +101,10 @@ class ItemListViewController: BaseViewController<ItemListCoordinator, ItemListVi
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

viewModel.bindImportObserverIfNeeded()
if didAppearForFirstTime {
didAppearForFirstTime = false
viewModel.viewDidAppear()
}
}

func addSubviews() {
Expand Down
7 changes: 4 additions & 3 deletions BookPlayer/Library/ItemList Screen/ItemListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ItemListViewModel: BaseViewModel<ItemListCoordinator> {
selectionHandler: (SimpleLibraryItem) -> Void
)
case showMiniPlayer(flag: Bool)
case bindImportObservers
case listDidAppear
}

enum Events {
Expand Down Expand Up @@ -119,8 +119,9 @@ class ItemListViewModel: BaseViewModel<ItemListCoordinator> {
bindDownloadObservers()
}

func bindImportObserverIfNeeded() {
onTransition?(.bindImportObservers)
/// Notify that the UI is presented and ready
func viewDidAppear() {
onTransition?(.listDidAppear)
}

func bindBookObservers() {
Expand Down
60 changes: 45 additions & 15 deletions BookPlayer/Player/PlayerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ final class PlayerManager: NSObject, PlayerManagerProtocol {
private var periodicTimeObserver: Any?
/// Flag determining if it should resume playback after finishing up loading an item
@Published private var playbackQueued: Bool?
/// Flag determining if it's in the process of fetching the URL for playback
@Published private var isFetchingRemoteURL: Bool?
private var hasObserverRegistered = false
private var observeStatus: Bool = false {
didSet {
Expand Down Expand Up @@ -133,10 +135,11 @@ final class PlayerManager: NSObject, PlayerManagerProtocol {
}
}

func loadRemoteURLAsset(for chapter: PlayableChapter) async throws -> AVURLAsset {
func loadRemoteURLAsset(for chapter: PlayableChapter, forceRefresh: Bool) async throws -> AVURLAsset {
let fileURL: URL

if let chapterURL = chapter.remoteURL {
if !forceRefresh,
let chapterURL = chapter.remoteURL {
fileURL = chapterURL
} else {
fileURL = try await syncService
Expand Down Expand Up @@ -183,14 +186,16 @@ final class PlayerManager: NSObject, PlayerManagerProtocol {
return asset
}

func loadPlayerItem(for chapter: PlayableChapter) async throws {
func loadPlayerItem(for chapter: PlayableChapter, forceRefreshURL: Bool) async throws {
let fileURL = DataManager.getProcessedFolderURL().appendingPathComponent(chapter.relativePath)

let asset: AVURLAsset

if syncService.isActive,
!FileManager.default.fileExists(atPath: fileURL.path) {
asset = try await loadRemoteURLAsset(for: chapter)
isFetchingRemoteURL = true
asset = try await loadRemoteURLAsset(for: chapter, forceRefresh: forceRefreshURL)
isFetchingRemoteURL = false
} else {
asset = AVURLAsset(url: fileURL, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
}
Expand All @@ -206,6 +211,10 @@ final class PlayerManager: NSObject, PlayerManagerProtocol {
}

func load(_ item: PlayableItem, autoplay: Bool) {
load(item, autoplay: autoplay, forceRefreshURL: false)
}

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

Expand Down Expand Up @@ -238,22 +247,23 @@ final class PlayerManager: NSObject, PlayerManagerProtocol {
NotificationCenter.default.post(name: .chapterChange, object: nil, userInfo: nil)
}

loadChapterMetadata(item.currentChapter, autoplay: autoplay)
loadChapterMetadata(item.currentChapter, autoplay: autoplay, forceRefreshURL: forceRefreshURL)
}

func loadChapterMetadata(_ chapter: PlayableChapter, autoplay: Bool? = nil) {
func loadChapterMetadata(_ chapter: PlayableChapter, autoplay: Bool? = nil, forceRefreshURL: Bool = false) {
if let autoplay {
playbackQueued = autoplay
}

loadChapterTask = Task { [unowned self] in
do {
try await self.loadPlayerItem(for: chapter)
try await self.loadPlayerItem(for: chapter, forceRefreshURL: forceRefreshURL)
self.loadChapterOperation(chapter)
} catch BookPlayerError.cancelledTask {
/// Do nothing, as it was cancelled to load another item
} catch {
self.playbackQueued = nil
self.isFetchingRemoteURL = nil
self.observeStatus = false
self.showErrorAlert(error.localizedDescription)
return
Expand All @@ -270,13 +280,15 @@ final class PlayerManager: NSObject, PlayerManagerProtocol {
else {
DispatchQueue.main.async {
self.currentItem = nil
self.isFetchingRemoteURL = nil
NotificationCenter.default.post(name: .bookReady, object: nil, userInfo: ["loaded": false])
}
return
}

self.audioPlayer.replaceCurrentItem(with: nil)
self.observeStatus = true
self.isFetchingRemoteURL = nil
self.audioPlayer.replaceCurrentItem(with: playerItem)

// Update UI on main thread
Expand Down Expand Up @@ -387,12 +399,17 @@ final class PlayerManager: NSObject, PlayerManagerProtocol {
}

func isPlayingPublisher() -> AnyPublisher<Bool, Never> {
return Publishers.CombineLatest(
return Publishers.CombineLatest3(
audioPlayer.publisher(for: \.timeControlStatus),
$playbackQueued
$playbackQueued,
$isFetchingRemoteURL
)
.map({ (timeControlStatus, playbackQueued) in
return timeControlStatus != .paused || playbackQueued == true
.map({ (timeControlStatus, playbackQueued, isFetchingRemoteURL) -> Bool in
let controlStatusFlag = timeControlStatus != .paused
let playbackQueuedFlag = playbackQueued == true
return controlStatusFlag
|| playbackQueuedFlag
|| (isFetchingRemoteURL == true && playbackQueuedFlag)
})
.eraseToAnyPublisher()
}
Expand Down Expand Up @@ -571,7 +588,11 @@ extension PlayerManager {
guard let playerItem else {
/// Check if the playbable item is in the process of being set
if observeStatus == false {
load(currentItem, autoplay: true)
if isFetchingRemoteURL == true {
playbackQueued = true
} else {
load(currentItem, autoplay: true)
}
}
return
}
Expand Down Expand Up @@ -670,9 +691,14 @@ extension PlayerManager {

guard item.status == .readyToPlay else {
if item.status == .failed {
playbackQueued = nil
observeStatus = false
showErrorAlert(item.error?.localizedDescription)
if (item.error as? NSError)?.code == NSURLErrorResourceUnavailable,
let currentItem {
loadAndRefreshURL(item: currentItem)
} else {
playbackQueued = nil
observeStatus = false
showErrorAlert(item.error?.localizedDescription)
}
}
return
}
Expand Down Expand Up @@ -842,6 +868,10 @@ extension PlayerManager {
libraryService.recursiveFolderProgressUpdate(from: parentFolder)
}
}

private func loadAndRefreshURL(item: PlayableItem) {
load(item, autoplay: playbackQueued == true, forceRefreshURL: true)
}
}

// MARK: - BookMarks
Expand Down
2 changes: 1 addition & 1 deletion BookPlayer/Profile/Profile Screen/ProfileViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class ProfileViewModel: ProfileViewModelProtocol {
}

func refreshSyncStatusMessage() {
let timestamp = UserDefaults.standard.double(forKey: Constants.UserDefaults.lastSyncTimestamp)
let timestamp = UserDefaults.standard.double(forKey: "\(Constants.UserDefaults.lastSyncTimestamp)_library")

guard timestamp > 0 else { return }

Expand Down
5 changes: 5 additions & 0 deletions BookPlayer/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
if let libraryCoordinator = mainCoordinator.getLibraryCoordinator() {
/// Sync list when app is active again
libraryCoordinator.syncList()
/// Sync currently shown list
let listCoordinator = libraryCoordinator.getLastItemListCoordinator(from: libraryCoordinator)
if listCoordinator != libraryCoordinator {
listCoordinator.syncList()
}
/// Register import observer in case it's not up already
libraryCoordinator.bindImportObserverIfNeeded()
}
Expand Down
6 changes: 5 additions & 1 deletion BookPlayerTests/Mocks/AccountServiceMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import Foundation
import RevenueCat

class AccountServiceMock: AccountServiceProtocol {
func hasActiveSubscription() -> Bool {
return account?.hasSubscription == true
}

var account: Account?

init(account: Account?) {
Expand Down Expand Up @@ -82,7 +86,7 @@ class AccountServiceMock: AccountServiceProtocol {
return try await Purchases.shared.customerInfo()
}

func loginIfUserExists() {}
func loginIfUserExists(delegate: PurchasesDelegate) {}

func login(with token: String, userId: String) async throws -> Account? {
self.account?.id = userId
Expand Down
8 changes: 6 additions & 2 deletions Shared/CoreData/CoreDataStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ public class CoreDataStack {
}

public func saveContext() {
guard self.managedContext.hasChanges else { return }
saveContext(managedContext)
}

public func saveContext(_ context: NSManagedObjectContext) {
guard context.hasChanges else { return }
do {
try self.managedContext.save()
try context.save()
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
Expand Down
Loading

0 comments on commit 281a313

Please sign in to comment.