Skip to content

Commit

Permalink
Add date support (#994)
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 Aug 30, 2024
1 parent 543408a commit 02125e7
Show file tree
Hide file tree
Showing 23 changed files with 147 additions and 84 deletions.
18 changes: 16 additions & 2 deletions Demo/Sources/Players/PlaybackView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -571,14 +571,28 @@ private struct TimeSlider: View {
return formatter
}()

private static let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .medium
return formatter
}()

@ObservedObject var player: Player
@ObservedObject var progressTracker: ProgressTracker
@ObservedObject var visibilityTracker: VisibilityTracker
@State private var streamType: StreamType = .unknown

private var formattedElapsedTime: String? {
guard streamType == .onDemand else { return nil }
return Self.formattedTime((progressTracker.time - progressTracker.timeRange.start), duration: progressTracker.timeRange.duration)
if streamType == .onDemand {
return Self.formattedTime((progressTracker.time - progressTracker.timeRange.start), duration: progressTracker.timeRange.duration)
}
else if let date = progressTracker.date() {
return Self.timeFormatter.string(from: date)
}
else {
return nil
}
}

private var formattedTotalTime: String? {
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Pillarbox player provides all essential playback features you might expect:
- Best-in-class Picture in Picture support.
- The smoothest possible seek experience on Apple devices, with blazing-fast content navigation in streams enabled for trick play.
- Playback speed controls.
- Looping playback.
- Monitoring with Pillarbox Quality of Experience (QoE) and Quality of Service (QoS) platform.

In addition Pillarbox provides the ability to play all SRG SSR content through a dedicated package.
Expand Down
3 changes: 2 additions & 1 deletion Sources/Player/Player+Navigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ extension Player {

private extension Player {
func isAwayFromStartTime(withInterval interval: TimeInterval) -> Bool {
time.isValid && seekableTimeRange.isValid && (time - seekableTimeRange.start).seconds >= interval
let time = time()
return time.isValid && seekableTimeRange.isValid && (time - seekableTimeRange.start).seconds >= interval
}

func shouldSeekToStartTime() -> Bool {
Expand Down
15 changes: 13 additions & 2 deletions Sources/Player/Player+Seek.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,28 @@ public extension Player {

/// Performs an optimal seek to a given time, providing the best possible interactive user experience in all cases.
///
/// For the best result the player should be paused during the whole interaction.
///
/// - Parameters:
/// - time: The time to reach.
/// - completion: A completion called when seeking ends. The provided Boolean informs whether the seek could
/// finish without being cancelled.
///
/// If a user interaction is causing this seek method to be called several times in a row, the player should be paused
/// during the interaction to achieve the best possible result.
func seek(to time: CMTime, completion: @escaping (Bool) -> Void = { _ in }) {
let position = Self.optimalPosition(reaching: time, for: queuePlayer.currentItem)
seek(position, smooth: true, completion: completion)
}

/// Requests that the player seek to a specified date, and to notify you when the seek is complete.
///
/// - Parameters:
/// - date: The date to reach.
/// - completion: A completion called when seeking ends. The provided Boolean informs whether the seek could
/// finish without being cancelled.
func seek(to date: Date, completion: @escaping (Bool) -> Void = { _ in }) {
queuePlayer.seek(to: date, completionHandler: completion)
}

/// Performs a precise seek to a chapter.
///
/// - Parameters:
Expand Down
4 changes: 2 additions & 2 deletions Sources/Player/Player+Skip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public extension Player {
func canSkipForward() -> Bool {
guard seekableTimeRange.isValidAndNotEmpty else { return false }
if duration.isIndefinite {
let currentTime = queuePlayer.targetSeekTime ?? time
let currentTime = queuePlayer.targetSeekTime ?? time()
return canSeek(to: currentTime + forwardSkipTime)
}
else {
Expand Down Expand Up @@ -64,7 +64,7 @@ private extension Player {
) {
assert(interval != .zero)
let endTolerance = CMTime(value: 1, timescale: 1)
let currentTime = queuePlayer.targetSeekTime ?? time
let currentTime = queuePlayer.targetSeekTime ?? time()
if interval < .zero || currentTime < seekableTimeRange.end - endTolerance {
seek(
to(currentTime + interval, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter),
Expand Down
2 changes: 1 addition & 1 deletion Sources/Player/Player+SkipToDefault.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public extension Player {
case .onDemand, .live:
return true
case .dvr where chunkDuration.isValid:
return time < seekableTimeRange.end - chunkDuration
return time() < seekableTimeRange.end - chunkDuration
default:
return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ To better understand how your playback experience is perceived by your users, yo

Pillarbox ``Player`` provides extensive metrics which can help you provide answer to these questions.

## Obtain metrics periodically
### Receive metrics when needed

You can receive current metrics at any time by calling the ``Player/metrics()`` player method. The same metrics are also available from ``PlayerProperties``.

## Receive metrics periodically

Subscribe to ``Player/periodicMetricsPublisher(forInterval:queue:limit:)`` to receive a stream of ``Metrics`` related to the item currently being played.

Expand Down
30 changes: 22 additions & 8 deletions Sources/Player/Player.docc/Extensions/player-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,12 @@
- ``selectedMediaOption(for:)``
- ``setMediaSelection(preferredLanguages:for:)``

#### Style
#### Behavior

- ``actionAtItemEnd``
- ``configuration``
- ``repeatMode``
- ``shouldPlay``
- ``textStyleRules``

### Navigation
Expand Down Expand Up @@ -73,9 +77,14 @@
- ``propertiesPublisher``
- ``rate``
- ``systemPlayer``
- ``time``
- ``version``

### Current State

- ``date()``
- ``metrics()``
- ``time()``

### Replay

- ``canReplay()``
Expand All @@ -87,18 +96,13 @@
- ``seek(_:smooth:completion:)``
- ``seek(to:completion:)-9bknb``
- ``seek(to:completion:)-2ypz8``
- ``seek(to:completion:)-1tbeq``
- ``after(_:)``
- ``at(_:)``
- ``before(_:)``
- ``near(_:)``
- ``to(_:toleranceBefore:toleranceAfter:)``

### Setup

- ``becomeActive()``
- ``configuration``
- ``resignActive()``

### Skip

- ``canSkipBackward()``
Expand All @@ -118,6 +122,11 @@
- ``canReturnToPrevious()``
- ``returnToPrevious()``

### System Integration

- ``becomeActive()``
- ``resignActive()``

### Time Publisher

- ``boundaryTimePublisher(for:queue:)``
Expand All @@ -128,6 +137,11 @@
- ``isTrackingEnabled``
- ``currentSessionIdentifiers(trackedBy:)``

### Metrics

- ``metricEventsPublisher``
- ``periodicMetricsPublisher(forInterval:queue:limit:)``

### User Interface

- ``mediaSelectionMenu(characteristic:)``
Expand Down
34 changes: 27 additions & 7 deletions Sources/Player/Player.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,6 @@ public final class Player: ObservableObject, Equatable {
}
}

/// The current time.
///
/// Returns `.invalid` when the time is unknown.
public var time: CMTime {
queuePlayer.currentTime().clamped(to: seekableTimeRange)
}

/// The low-level system player.
///
/// Exposed for specific read-only needs like interfacing with `AVPlayer`-based 3rd party APIs. Mutating the state
Expand Down Expand Up @@ -287,6 +280,33 @@ public final class Player: ObservableObject, Equatable {
}
}

public extension Player {
/// The current time.
///
/// Returns `.invalid` when the time is unknown.
func time() -> CMTime {
properties.time()
}

/// The current date.
///
/// The date is `nil` when no date information is available from the stream.
func date() -> Date? {
properties.date()
}

/// The current player metrics, if available.
///
/// Each call to this function might return different results reflecting the most recent metrics available. The
/// included ``Metrics/increment`` collates data from the entire playback session and is therefore always equal
/// to ``Metrics/total``.
///
/// > Important: Metrics are reset when toggling external playback.
func metrics() -> Metrics? {
properties.metrics()
}
}

private extension Player {
func configurePublishedPropertyPublishers() {
configurePropertiesPublisher()
Expand Down
6 changes: 3 additions & 3 deletions Sources/Player/Types/PlayerProperties.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,14 @@ public extension PlayerProperties {
}

public extension PlayerProperties {
/// The player time.
/// The current time.
func time() -> CMTime {
coreProperties.time()
}

/// The date corresponding to the player time.
/// The current date.
///
/// This date is only returned when available from the stream.
/// The date is `nil` when no date information is available from the stream.
func date() -> Date? {
coreProperties.date()
}
Expand Down
10 changes: 9 additions & 1 deletion Sources/Player/UserInterface/ProgressTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public final class ProgressTracker: ObservableObject {
.map { time, seekableTimeRange in
Self.progress(for: time, in: seekableTimeRange)
}
.prepend(Self.progress(for: player.time, in: player.seekableTimeRange))
.prepend(Self.progress(for: player.time(), in: player.seekableTimeRange))
.eraseToAnyPublisher()
}
.switchToLatest()
Expand Down Expand Up @@ -177,6 +177,14 @@ public final class ProgressTracker: ObservableObject {
return timeRange.start + CMTimeMultiplyByFloat64(timeRange.duration, multiplier: Float64(progress))
}

/// The date corresponding to the current progress.
///
/// The date is `nil` when no date information is available from the stream.
public func date() -> Date? {
guard let player, let playerDate = player.date() else { return nil }
return playerDate.addingTimeInterval((time - player.time()).seconds)
}

private func seek(to progress: Float, optimal: Bool) {
guard let player else { return }
let time = Self.time(forProgress: progress, in: timeRange)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ final class ComScoreTrackerDvrPropertiesTests: ComScoreTestCase {
expect(labels.ns_st_ldw).to(equal(Stream.dvr.duration.seconds))
}
) {
player.seek(at(player.time - CMTime(value: 10, timescale: 1)))
player.seek(at(player.time() - CMTime(value: 10, timescale: 1)))
}
}
}
6 changes: 3 additions & 3 deletions Tests/AnalyticsTests/ComScore/ComScoreTrackerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ final class ComScoreTrackerTests: ComScoreTestCase {
))

player.play()
expect(player.time.seconds).toEventually(beGreaterThan(1))
expect(player.time().seconds).toEventually(beGreaterThan(1))

expectAtLeastHits(
pause { labels in
Expand Down Expand Up @@ -120,7 +120,7 @@ final class ComScoreTrackerTests: ComScoreTestCase {
// See 2. at the top of this file.
player?.play()
// See 1. at the top of this file.
expect(player?.time.seconds).toEventually(beGreaterThan(5))
expect(player?.time().seconds).toEventually(beGreaterThan(5))
player = nil
}
}
Expand Down Expand Up @@ -148,7 +148,7 @@ final class ComScoreTrackerTests: ComScoreTestCase {
// See 2. at the top of this file.
player.play()
// See 1. at the top of this file.
expect(player.time.seconds).toEventually(beGreaterThan(5))
expect(player.time().seconds).toEventually(beGreaterThan(5))
player.isTrackingEnabled = false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ final class CommandersActTrackerDvrPropertiesTests: CommandersActTestCase {
expect(labels.media_timeshift).to(beCloseTo(4, within: 2))
}
) {
player.seek(at(player.time - CMTime(value: 4, timescale: 1)))
player.seek(at(player.time() - CMTime(value: 4, timescale: 1)))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ final class CommandersActTrackerTests: CommandersActTestCase {
))

player.play()
expect(player.time.seconds).toEventually(beGreaterThan(1))
expect(player.time().seconds).toEventually(beGreaterThan(1))

expectAtLeastHits(
pause { labels in
Expand Down Expand Up @@ -85,7 +85,7 @@ final class CommandersActTrackerTests: CommandersActTestCase {
))

player?.play()
expect(player?.time.seconds).toEventually(beGreaterThan(5))
expect(player?.time().seconds).toEventually(beGreaterThan(5))

expectAtLeastHits(
stop { labels in
Expand All @@ -106,7 +106,7 @@ final class CommandersActTrackerTests: CommandersActTestCase {
player?.setDesiredPlaybackSpeed(2)

player?.play()
expect(player?.time.seconds).toEventually(beGreaterThan(2))
expect(player?.time().seconds).toEventually(beGreaterThan(2))

expectAtLeastHits(
stop { labels in
Expand Down Expand Up @@ -155,7 +155,7 @@ final class CommandersActTrackerTests: CommandersActTestCase {
))

player.play()
expect(player.time.seconds).toEventually(beGreaterThan(5))
expect(player.time().seconds).toEventually(beGreaterThan(5))

expectAtLeastHits(stop()) {
player.isTrackingEnabled = false
Expand Down
Loading

0 comments on commit 02125e7

Please sign in to comment.