Skip to content

Commit

Permalink
[iOS] Persist playback position when the app resigns or terminates
Browse files Browse the repository at this point in the history
This change adds back 2 notifications which the old playlist UI used to persist the currently playing item's current position
  • Loading branch information
kylehickinson committed Dec 2, 2024
1 parent a6b2da5 commit b98ed4f
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 12 deletions.
39 changes: 32 additions & 7 deletions ios/brave-ios/Sources/PlaylistUI/PlayerModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 },
])
}

Expand Down
5 changes: 1 addition & 4 deletions ios/brave-ios/Sources/PlaylistUI/PlaylistContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
40 changes: 39 additions & 1 deletion ios/brave-ios/Tests/PlaylistUITests/PlayerModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit b98ed4f

Please sign in to comment.