Skip to content

Commit

Permalink
Fix incorrectly received tracker property updates (#770)
Browse files Browse the repository at this point in the history
Co-authored-by: Walid Kayhal <[email protected]>
  • Loading branch information
defagos and waliid authored Feb 15, 2024
1 parent 801834e commit 4810507
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 146 deletions.
14 changes: 14 additions & 0 deletions Sources/Player/Asset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ public struct Asset<M>: Assetable where M: AssetMetadata {
private let configuration: (AVPlayerItem) -> Void
private let trackerAdapters: [TrackerAdapter<M>]

init(
id: UUID,
resource: Resource,
metadata: M?,
configuration: @escaping (AVPlayerItem) -> Void,
trackerAdapters: [TrackerAdapter<M>]
) {
self.id = id
self.resource = resource
self.metadata = metadata
self.configuration = configuration
self.trackerAdapters = trackerAdapters.map { $0.withId(id) }
}

/// Returns a simple asset playable from a URL.
///
/// - Parameters:
Expand Down
2 changes: 1 addition & 1 deletion Sources/Player/Player.swift
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ private extension Player {
.sink { [weak self] nowPlayingInfoMetadata, nowPlayingInfoPlayback, isActive in
guard let self else { return }
let nowPlayingInfo = isActive ? nowPlayingInfoMetadata.merging(nowPlayingInfoPlayback) { _, new in new } : [:]
self.updateControlCenter(nowPlayingInfo: nowPlayingInfo)
updateControlCenter(nowPlayingInfo: nowPlayingInfo)
}
.store(in: &cancellables)
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/Player/Publishers/AVPlayerItemPublishers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ extension AVPlayerItem {
publisher(for: \.duration),
minimumTimeOffsetFromLivePublisher()
)
.map { state, presentationSize, mediaSelectionProperties, timeProperties, duration, minimumTimeOffsetFromLive in
.map { [weak self] state, presentationSize, mediaSelectionProperties, timeProperties, duration, minimumTimeOffsetFromLive in
let isKnown = (state != .unknown)
return .init(
itemProperties: .init(
item: self,
state: state,
duration: isKnown ? duration : .invalid,
minimumTimeOffsetFromLive: minimumTimeOffsetFromLive,
Expand Down
10 changes: 9 additions & 1 deletion Sources/Player/Tracking/TrackerAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//

import Combine
import Foundation

/// An adapter which instantiates and manages a tracker of a specified type.
///
Expand All @@ -13,6 +14,7 @@ public class TrackerAdapter<M: AssetMetadata> {
private let tracker: any PlayerItemTracker
private let update: (M) -> Void
private var cancellables = Set<AnyCancellable>()
private var id = UUID()

/// Creates an adapter for a type of tracker with the provided mapping to its metadata format.
///
Expand All @@ -30,12 +32,18 @@ public class TrackerAdapter<M: AssetMetadata> {
self.tracker = tracker
}

func withId(_ id: UUID) -> Self {
self.id = id
return self
}

func enable(for player: Player) {
tracker.enable(for: player)

player.propertiesPublisher
.sink { [weak self] properties in
self?.tracker.updateProperties(with: properties)
guard let self, properties.id == id else { return }
tracker.updateProperties(with: properties)
}
.store(in: &cancellables)
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/Player/Types/ItemProperties.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@
// License information is available from the LICENSE file.
//

import AVFoundation
import CoreMedia

struct ItemProperties: Equatable {
static var empty: Self {
.init(
item: nil,
state: .unknown,
duration: .invalid,
minimumTimeOffsetFromLive: .invalid,
presentationSize: nil
)
}

let item: AVPlayerItem?
let state: ItemState

let duration: CMTime
Expand Down
4 changes: 4 additions & 0 deletions Sources/Player/Types/PlayerProperties.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public struct PlayerProperties: Equatable {
/// The time at which the player is currently seeking, if any.
public let seekTime: CMTime?

var id: UUID {
coreProperties.itemProperties.item?.id ?? UUID()
}

init(
coreProperties: CoreProperties,
timeProperties: TimeProperties,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
@testable import PillarboxPlayer

import Combine
import Foundation

final class NoMetadataTrackerMock: PlayerItemTracker {
final class TrackerLifeCycleMock: PlayerItemTracker {
typealias StatePublisher = PassthroughSubject<State, Never>

enum State {
enum State: Equatable {
case initialized
case enabled
case disabled
case updated
case deinitialized
}

Expand All @@ -31,9 +31,7 @@ final class NoMetadataTrackerMock: PlayerItemTracker {
configuration.statePublisher.send(.initialized)
}

func updateMetadata(with metadata: Void) {
configuration.statePublisher.send(.updated)
}
func updateMetadata(with metadata: Void) {}

func updateProperties(with properties: PlayerProperties) {}

Expand All @@ -50,7 +48,7 @@ final class NoMetadataTrackerMock: PlayerItemTracker {
}
}

extension NoMetadataTrackerMock {
extension TrackerLifeCycleMock {
static func adapter(statePublisher: StatePublisher) -> TrackerAdapter<Never> {
adapter(configuration: Configuration(statePublisher: statePublisher))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
@testable import PillarboxPlayer

import Combine
import Foundation

final class TrackerMock<Metadata>: PlayerItemTracker where Metadata: Equatable {
final class TrackerUpdateMock<Metadata>: PlayerItemTracker where Metadata: Equatable {
typealias StatePublisher = PassthroughSubject<State, Never>

enum State: Equatable {
case initialized
case enabled
case disabled
case updated(Metadata)
case deinitialized
case updatedMetadata(Metadata)
case updatedProperties(for: UUID)
}

struct Configuration {
Expand All @@ -28,30 +28,31 @@ final class TrackerMock<Metadata>: PlayerItemTracker where Metadata: Equatable {

init(configuration: Configuration) {
self.configuration = configuration
configuration.statePublisher.send(.initialized)
}

func enable(for player: Player) {
configuration.statePublisher.send(.enabled)
}

func updateMetadata(with metadata: Metadata) {
configuration.statePublisher.send(.updated(metadata))
configuration.statePublisher.send(.updatedMetadata(metadata))
}

func updateProperties(with properties: PlayerProperties) {}
func updateProperties(with properties: PlayerProperties) {
configuration.statePublisher.send(.updatedProperties(for: properties.id))
}

func disable() {
configuration.statePublisher.send(.disabled)
}

deinit {
configuration.statePublisher.send(.deinitialized)
}
}

extension TrackerMock {
extension TrackerUpdateMock {
static func adapter<M>(statePublisher: StatePublisher, mapper: @escaping (M) -> Metadata) -> TrackerAdapter<M> where M: AssetMetadata {
adapter(configuration: Configuration(statePublisher: statePublisher), mapper: mapper)
}

static func adapter(statePublisher: StatePublisher) -> TrackerAdapter<Never> {
adapter(configuration: Configuration(statePublisher: statePublisher))
}
}
72 changes: 72 additions & 0 deletions Tests/PlayerTests/Tracking/PlayerItemTrackerLifeCycleTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// Copyright (c) SRG SSR. All rights reserved.
//
// License information is available from the LICENSE file.
//

@testable import PillarboxPlayer

import Foundation
import PillarboxCircumspect
import PillarboxStreams

final class PlayerItemTrackerLifeCycleTests: TestCase {
func testPlayerItemLifeCycle() {
let publisher = TrackerLifeCycleMock.StatePublisher()
expectAtLeastEqualPublished(values: [.initialized, .deinitialized], from: publisher) {
_ = PlayerItem.simple(
url: Stream.shortOnDemand.url,
trackerAdapters: [TrackerLifeCycleMock.adapter(statePublisher: publisher)]
)
}
}

func testItemPlayback() {
let player = Player()
let publisher = TrackerLifeCycleMock.StatePublisher()
expectEqualPublished(values: [.initialized, .enabled], from: publisher, during: .seconds(2)) {
player.append(.simple(
url: Stream.onDemand.url,
trackerAdapters: [TrackerLifeCycleMock.adapter(statePublisher: publisher)]
))
player.play()
}
}

func testItemEntirePlayback() {
let player = Player()
let publisher = TrackerLifeCycleMock.StatePublisher()
expectAtLeastEqualPublished(values: [.initialized, .enabled, .disabled], from: publisher) {
player.append(.simple(
url: Stream.shortOnDemand.url,
trackerAdapters: [TrackerLifeCycleMock.adapter(statePublisher: publisher)]
))
player.play()
}
}

func testNetworkLoadedItemEntirePlayback() {
let player = Player()
let publisher = TrackerLifeCycleMock.StatePublisher()
expectAtLeastEqualPublished(values: [.initialized, .enabled, .disabled], from: publisher) {
player.append(.mock(
url: Stream.shortOnDemand.url,
loadedAfter: 1,
trackerAdapters: [TrackerLifeCycleMock.adapter(statePublisher: publisher)]
))
player.play()
}
}

func testFailedItem() {
let player = Player()
let publisher = TrackerLifeCycleMock.StatePublisher()
expectEqualPublished(values: [.initialized, .enabled], from: publisher, during: .milliseconds(500)) {
player.append(.simple(
url: Stream.unavailable.url,
trackerAdapters: [TrackerLifeCycleMock.adapter(statePublisher: publisher)]
))
player.play()
}
}
}
98 changes: 0 additions & 98 deletions Tests/PlayerTests/Tracking/PlayerItemTrackerTests.swift

This file was deleted.

Loading

0 comments on commit 4810507

Please sign in to comment.