You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
45 lines
1.8 KiB
45 lines
1.8 KiB
/// Counts received and missing video frames from NDI timestamps.
|
|
///
|
|
/// A gap larger than 1.5 frame durations counts the frames that should have arrived in between.
|
|
/// Gaps longer than `stallThreshold` are treated as an outage (reconnect), not as lost frames.
|
|
public struct FrameStats: Sendable, Equatable {
|
|
public private(set) var received: Int64 = 0
|
|
public private(set) var missing: Int64 = 0
|
|
public private(set) var droppedByReceiver: Int64 = 0
|
|
private var lastTimestamp: Int64?
|
|
|
|
/// NDI timestamps are in 100 ns units.
|
|
public static let ticksPerSecond: Double = 10_000_000
|
|
public var stallThreshold: Double = 2.0 // seconds
|
|
|
|
public init() {}
|
|
|
|
/// Lost frames relative to all frames that should have been shown.
|
|
public var lossRate: Double {
|
|
let lost = missing + droppedByReceiver
|
|
let expected = received + lost
|
|
return expected == 0 ? 0 : Double(lost) / Double(expected)
|
|
}
|
|
|
|
public mutating func record(timestamp: Int64, frameRateN: Int32, frameRateD: Int32) {
|
|
received += 1
|
|
defer { lastTimestamp = timestamp }
|
|
guard let last = lastTimestamp, frameRateN > 0, frameRateD > 0 else { return }
|
|
|
|
let delta = Double(timestamp - last) / Self.ticksPerSecond
|
|
let frameDuration = Double(frameRateD) / Double(frameRateN)
|
|
guard delta > frameDuration * 1.5, delta < stallThreshold else { return }
|
|
missing += Int64((delta / frameDuration).rounded()) - 1
|
|
}
|
|
|
|
/// Total frames NDI itself dropped because we did not capture fast enough.
|
|
public mutating func updateDroppedByReceiver(_ total: Int64) {
|
|
droppedByReceiver = total
|
|
}
|
|
|
|
/// Call after a reconnect so the outage is not counted as lost frames.
|
|
public mutating func resetTimeline() {
|
|
lastTimestamp = nil
|
|
}
|
|
}
|