-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Walid Kayhal <[email protected]>
- Loading branch information
Showing
28 changed files
with
1,087 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// | ||
// Copyright (c) SRG SSR. All rights reserved. | ||
// | ||
// License information is available from the LICENSE file. | ||
// | ||
|
||
import AVFoundation | ||
|
||
struct AccessLog { | ||
let closedEvents: [AccessLogEvent] | ||
let openEvent: AccessLogEvent? | ||
|
||
init(events: [AccessLogEvent?], after date: Date?) { | ||
closedEvents = Array(events.prefix(max(events.count - 1, 0))).compactMap { event in | ||
Self.event(event, after: date) | ||
} | ||
if let lastEvent = events.last { | ||
openEvent = Self.event(lastEvent, after: date) | ||
} | ||
else { | ||
openEvent = nil | ||
} | ||
} | ||
|
||
init(_ log: AVPlayerItemAccessLog, after date: Date?) { | ||
self.init(events: log.events.map { .init($0) }, after: date) | ||
} | ||
|
||
static func event(_ event: AccessLogEvent?, after date: Date?) -> AccessLogEvent? { | ||
guard let date, let event else { return event } | ||
return event.playbackStartDate > date ? event : nil | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
// | ||
// Copyright (c) SRG SSR. All rights reserved. | ||
// | ||
// License information is available from the LICENSE file. | ||
// | ||
|
||
import AVFoundation | ||
|
||
struct AccessLogEvent: Equatable { | ||
let playbackStartDate: Date | ||
|
||
// MARK: Optional information | ||
|
||
let uri: String? | ||
let serverAddress: String? | ||
let playbackSessionId: String? | ||
let playbackStartOffset: TimeInterval? | ||
let playbackType: String? | ||
let startupTime: TimeInterval? | ||
let observedBitrateStandardDeviation: Double? | ||
let indicatedBitrate: Double? | ||
let observedBitrate: Double? | ||
let averageAudioBitrate: Double? | ||
let averageVideoBitrate: Double? | ||
let indicatedAverageBitrate: Double? | ||
|
||
// MARK: Additive information | ||
|
||
let numberOfServerAddressChanges: Int | ||
let mediaRequestsWWAN: Int | ||
let transferDuration: TimeInterval | ||
let numberOfBytesTransferred: Int64 | ||
let numberOfMediaRequests: Int | ||
let playbackDuration: TimeInterval | ||
let numberOfDroppedVideoFrames: Int | ||
let numberOfStalls: Int | ||
let segmentsDownloadedDuration: TimeInterval | ||
let downloadOverdue: Int | ||
let switchBitrate: Double | ||
|
||
init?( | ||
playbackStartDate: Date?, | ||
uri: String?, | ||
serverAddress: String?, | ||
playbackSessionId: String?, | ||
playbackStartOffset: TimeInterval, | ||
playbackType: String?, | ||
startupTime: TimeInterval, | ||
observedBitrateStandardDeviation: Double, | ||
indicatedBitrate: Double, | ||
observedBitrate: Double, | ||
averageAudioBitrate: Double, | ||
averageVideoBitrate: Double, | ||
indicatedAverageBitrate: Double, | ||
numberOfServerAddressChanges: Int, | ||
mediaRequestsWWAN: Int, | ||
transferDuration: TimeInterval, | ||
numberOfBytesTransferred: Int64, | ||
numberOfMediaRequests: Int, | ||
playbackDuration: TimeInterval, | ||
numberOfDroppedVideoFrames: Int, | ||
numberOfStalls: Int, | ||
segmentsDownloadedDuration: TimeInterval, | ||
downloadOverdue: Int, | ||
switchBitrate: Double | ||
) { | ||
guard let playbackStartDate else { return nil } | ||
self.playbackStartDate = playbackStartDate | ||
|
||
self.uri = uri | ||
self.serverAddress = serverAddress | ||
self.playbackSessionId = playbackSessionId | ||
self.playbackStartOffset = Self.optional(playbackStartOffset) | ||
self.playbackType = playbackType | ||
self.startupTime = Self.optional(startupTime) | ||
self.observedBitrateStandardDeviation = Self.optional(observedBitrateStandardDeviation) | ||
self.indicatedBitrate = Self.optional(indicatedBitrate) | ||
self.observedBitrate = Self.optional(observedBitrate) | ||
self.averageAudioBitrate = Self.optional(averageAudioBitrate) | ||
self.averageVideoBitrate = Self.optional(averageVideoBitrate) | ||
self.indicatedAverageBitrate = Self.optional(indicatedAverageBitrate) | ||
|
||
self.numberOfServerAddressChanges = Self.nonNegative(numberOfServerAddressChanges) | ||
self.mediaRequestsWWAN = Self.nonNegative(mediaRequestsWWAN) | ||
self.transferDuration = Self.nonNegative(transferDuration) | ||
self.numberOfBytesTransferred = Self.nonNegative(numberOfBytesTransferred) | ||
self.numberOfMediaRequests = Self.nonNegative(numberOfMediaRequests) | ||
self.playbackDuration = Self.nonNegative(playbackDuration) | ||
self.numberOfDroppedVideoFrames = Self.nonNegative(numberOfDroppedVideoFrames) | ||
self.numberOfStalls = Self.nonNegative(numberOfStalls) | ||
self.segmentsDownloadedDuration = Self.nonNegative(segmentsDownloadedDuration) | ||
self.downloadOverdue = Self.nonNegative(downloadOverdue) | ||
self.switchBitrate = Self.nonNegative(switchBitrate) | ||
} | ||
} | ||
|
||
extension AccessLogEvent { | ||
init?(_ event: AVPlayerItemAccessLogEvent) { | ||
self.init( | ||
playbackStartDate: event.playbackStartDate, | ||
uri: event.uri, | ||
serverAddress: event.serverAddress, | ||
playbackSessionId: event.playbackSessionID, | ||
playbackStartOffset: event.playbackStartOffset, | ||
playbackType: event.playbackType, | ||
startupTime: event.startupTime, | ||
observedBitrateStandardDeviation: event.observedBitrateStandardDeviation, | ||
indicatedBitrate: event.indicatedBitrate, | ||
observedBitrate: event.observedBitrate, | ||
averageAudioBitrate: event.averageAudioBitrate, | ||
averageVideoBitrate: event.averageVideoBitrate, | ||
indicatedAverageBitrate: event.indicatedAverageBitrate, | ||
numberOfServerAddressChanges: event.numberOfServerAddressChanges, | ||
mediaRequestsWWAN: event.mediaRequestsWWAN, | ||
transferDuration: event.transferDuration, | ||
numberOfBytesTransferred: event.numberOfBytesTransferred, | ||
numberOfMediaRequests: event.numberOfMediaRequests, | ||
playbackDuration: event.durationWatched, | ||
numberOfDroppedVideoFrames: event.numberOfDroppedVideoFrames, | ||
numberOfStalls: event.numberOfStalls, | ||
segmentsDownloadedDuration: event.segmentsDownloadedDuration, | ||
downloadOverdue: event.downloadOverdue, | ||
switchBitrate: event.switchBitrate | ||
) | ||
} | ||
} | ||
|
||
extension AccessLogEvent { | ||
static func nonNegative<T>(_ value: T) -> T where T: Comparable & SignedNumeric { | ||
max(value, .zero) | ||
} | ||
|
||
static func optional<T>(_ value: T) -> T? where T: Comparable & SignedNumeric { | ||
value < .zero ? nil : value | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// | ||
// Copyright (c) SRG SSR. All rights reserved. | ||
// | ||
// License information is available from the LICENSE file. | ||
// | ||
|
||
import CoreMedia | ||
import Foundation | ||
|
||
/// An object used to capture metrics associated with a player. | ||
public struct Metrics: Equatable { | ||
/// The date and time at which playback began. | ||
public let playbackStartDate: Date | ||
|
||
/// The player time at the time metrics were captured. | ||
public let time: CMTime | ||
|
||
// MARK: Server information | ||
|
||
/// The URI of the playback item. | ||
public let uri: String? | ||
|
||
/// The IP address of the server that was the source of the last delivered media segment. | ||
public let serverAddress: String? | ||
|
||
// MARK: Playback-related information | ||
|
||
/// A GUID that identifies the current playback session. | ||
public let playbackSessionId: String? | ||
|
||
/// The offset, in seconds, in the playlist where the last uninterrupted period of playback began. | ||
public let playbackStartOffset: TimeInterval? | ||
|
||
/// The playback type. | ||
public let playbackType: String? | ||
|
||
/// The accumulated duration, in seconds, until the player item is ready to play. | ||
public let startupTime: TimeInterval? | ||
|
||
// MARK: Bitrate information | ||
|
||
/// The standard deviation of the observed segment download bit rates. | ||
/// | ||
/// Measures actual network download performance. | ||
public let observedBitrateStandardDeviation: Double? | ||
|
||
/// The throughput, in bits per second, required to play the stream, as advertised by the server. | ||
public let indicatedBitrate: Double? | ||
|
||
/// The empirical throughput, in bits per second, across all media downloaded. | ||
/// | ||
/// Measures actual network download performance. Provides a bandwidth estimate. | ||
public let observedBitrate: Double? | ||
|
||
/// The audio track’s average bit rate, in bits per second. | ||
public let averageAudioBitrate: Double? | ||
|
||
/// The video track’s average bit rate, in bits per second. | ||
public let averageVideoBitrate: Double? | ||
|
||
/// The average throughput, in bits per second, required to play the stream, as advertised by the server. | ||
public let indicatedAverageBitrate: Double? | ||
|
||
// MARK: Values | ||
|
||
/// The associated increment. | ||
public let increment: MetricsValues | ||
|
||
/// The associated total. | ||
public let total: MetricsValues | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// | ||
// Copyright (c) SRG SSR. All rights reserved. | ||
// | ||
// License information is available from the LICENSE file. | ||
// | ||
|
||
import AVFoundation | ||
|
||
struct MetricsState: Equatable { | ||
static let empty = Self(time: .invalid, event: nil, total: .zero, cache: .empty) | ||
|
||
private let time: CMTime | ||
private let event: AccessLogEvent? | ||
private let total: MetricsValues | ||
let cache: Cache | ||
|
||
func updated(with log: AccessLog, at time: CMTime) -> Self? { | ||
let cache = cache.updated(with: log) | ||
if let openEvent = log.openEvent { | ||
return .init(time: time, event: openEvent, total: cache.total + .values(from: openEvent), cache: cache) | ||
} | ||
else if let lastClosedEvent = log.closedEvents.last { | ||
return .init(time: time, event: lastClosedEvent, total: cache.total, cache: cache) | ||
} | ||
else { | ||
return nil | ||
} | ||
} | ||
|
||
func updated(with log: AVPlayerItemAccessLog?, at time: CMTime) -> Self? { | ||
guard let log else { return nil } | ||
return updated(with: .init(log, after: cache.date), at: time) | ||
} | ||
|
||
func metrics(from state: Self) -> Metrics? { | ||
guard let event else { return nil } | ||
return .init( | ||
playbackStartDate: event.playbackStartDate, | ||
time: time, | ||
uri: event.uri, | ||
serverAddress: event.serverAddress, | ||
playbackSessionId: event.playbackSessionId, | ||
playbackStartOffset: event.playbackStartOffset, | ||
playbackType: event.playbackType, | ||
startupTime: event.startupTime, | ||
observedBitrateStandardDeviation: event.observedBitrateStandardDeviation, | ||
indicatedBitrate: event.indicatedBitrate, | ||
observedBitrate: event.observedBitrate, | ||
averageAudioBitrate: event.averageAudioBitrate, | ||
averageVideoBitrate: event.averageVideoBitrate, | ||
indicatedAverageBitrate: event.indicatedAverageBitrate, | ||
increment: total - state.total, | ||
total: total | ||
) | ||
} | ||
} | ||
|
||
extension MetricsState { | ||
struct Cache: Equatable { | ||
static let empty = Self(date: nil, total: .zero) | ||
|
||
let date: Date? | ||
let total: MetricsValues | ||
|
||
func updated(with log: AccessLog) -> Self { | ||
guard let lastEvent = log.closedEvents.last else { return self } | ||
return .init( | ||
date: lastEvent.playbackStartDate, | ||
total: log.closedEvents.reduce(total) { initial, next in | ||
initial + .values(from: next) | ||
} | ||
) | ||
} | ||
} | ||
} |
Oops, something went wrong.