diff --git a/ios/brave-ios/Sources/PlaylistUI/PlayerModel.swift b/ios/brave-ios/Sources/PlaylistUI/PlayerModel.swift index d62c5fbf139f..1d60026504d8 100644 --- a/ios/brave-ios/Sources/PlaylistUI/PlayerModel.swift +++ b/ios/brave-ios/Sources/PlaylistUI/PlayerModel.swift @@ -391,9 +391,8 @@ public final class PlayerModel: ObservableObject { let currentTime = currentTime let duration = duration.seconds ?? 0 // Reset the current item's last played time if you changed videos in the last 10s - PlaylistManager.shared.updateLastPlayed( - item: .init(item: selectedItem), - playTime: (duration - 10...duration).contains(currentTime) ? 0 : currentTime + persistPlaybackTimeForSelectedItem( + (duration - 10...duration).contains(currentTime) ? 0 : currentTime ) } } @@ -682,6 +681,11 @@ public final class PlayerModel: ObservableObject { return .seconds(duration.seconds) } + @MainActor func persistPlaybackTimeForSelectedItem(_ time: TimeInterval) { + guard let selectedItem else { return } + PlaylistManager.shared.updateLastPlayed(item: .init(item: selectedItem), playTime: time) + } + // MARK: - private let player: AVPlayer = .init() @@ -712,10 +716,7 @@ public final class PlayerModel: ObservableObject { MainActor.assumeIsolated { if let selectedItem = self.selectedItem { // Reset the play time of the item that just finished - PlaylistManager.shared.updateLastPlayed( - item: .init(item: selectedItem), - playTime: 0 - ) + self.persistPlaybackTimeForSelectedItem(0) } if case .itemPlaybackCompletion = self.sleepTimerCondition { self.pause() @@ -779,11 +780,35 @@ public final class PlayerModel: ObservableObject { playerLayer.player = player } + let willResignActive = center.addObserver( + forName: UIApplication.willResignActiveNotification, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self else { return } + MainActor.assumeIsolated { + self.persistPlaybackTimeForSelectedItem(self.currentTime) + } + } + + let willTerminate = center.addObserver( + forName: UIApplication.willTerminateNotification, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self else { return } + MainActor.assumeIsolated { + self.persistPlaybackTimeForSelectedItem(self.currentTime) + } + } + cancellables.formUnion([ .init { _ = didPlayToEndTime }, .init { _ = interruption }, .init { _ = didEnterBackground }, .init { _ = willEnterForeground }, + .init { _ = willResignActive }, + .init { _ = willTerminate }, ]) } diff --git a/ios/brave-ios/Sources/PlaylistUI/PlaylistContentView.swift b/ios/brave-ios/Sources/PlaylistUI/PlaylistContentView.swift index 799acec2426f..51ab09eee000 100644 --- a/ios/brave-ios/Sources/PlaylistUI/PlaylistContentView.swift +++ b/ios/brave-ios/Sources/PlaylistUI/PlaylistContentView.swift @@ -161,10 +161,7 @@ struct PlaylistContentView: View { } .onDisappear { if let selectedItem { - PlaylistManager.shared.updateLastPlayed( - item: .init(item: selectedItem), - playTime: playerModel.currentTime - ) + playerModel.persistPlaybackTimeForSelectedItem(playerModel.currentTime) } } .sheet(isPresented: $isEditModePresented) { diff --git a/ios/brave-ios/Tests/PlaylistUITests/PlayerModelTests.swift b/ios/brave-ios/Tests/PlaylistUITests/PlayerModelTests.swift index 415c69bb4f6b..825606ec7640 100644 --- a/ios/brave-ios/Tests/PlaylistUITests/PlayerModelTests.swift +++ b/ios/brave-ios/Tests/PlaylistUITests/PlayerModelTests.swift @@ -4,13 +4,13 @@ // You can obtain one at https://mozilla.org/MPL/2.0/. import AVFoundation -import Data import Foundation import Playlist import Preferences import TestHelpers import XCTest +@testable import Data @testable import PlaylistUI class PlayerModelTests: CoreDataTestCase { @@ -357,6 +357,44 @@ class PlayerModelTests: CoreDataTestCase { XCTAssertEqual(playerModel.currentTime, 3) } + /// Tests that playback is correctly saved when the app resigns being active + @MainActor func testSavingPlaybackOnAppResign() async throws { + let folder = try await addFolder() + await addMockItems(count: 1, to: folder) + + Preferences.Playlist.playbackLeftOff.value = true + + let item = try XCTUnwrap(PlaylistItem.getItems(parentFolder: folder).first) + XCTAssertEqual(item.lastPlayedOffset, 0) + + let playerModel = PlayerModel(mediaStreamer: nil, initialPlaybackInfo: nil) + playerModel.selectedFolderID = folder.id + await playerModel.prepareItemQueue() + + XCTAssertEqual(playerModel.currentTime, 0) + await playerModel.seek(to: 3, accurately: true) + XCTAssertEqual(playerModel.currentTime, 3) + + NotificationCenter.default.post(name: UIApplication.willResignActiveNotification, object: nil) + + let mergeExpectation = expectation(description: "merge") + let saveExpectation = expectation( + forNotification: .NSManagedObjectContextDidSave, + object: nil + ) { notification in + DispatchQueue.main.async { + DataController.viewContext.mergeChanges(fromContextDidSave: notification) + mergeExpectation.fulfill() + } + return true + } + + await fulfillment(of: [mergeExpectation, saveExpectation], timeout: 1) + + let lastPlayedOffset = PlaylistItem.getItem(id: item.id)?.lastPlayedOffset + XCTAssertEqual(lastPlayedOffset, 3) + } + /// Test having both an initial offset passed in and the playbackLeftOff preference being enabled @MainActor func testResumingPlaybackWithBothLastPlayedOffsetAndInitialItem() async throws { let folder = try await addFolder()