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.
Musubi/Packages/MusubiKit/Sources/MusubiCore/FrameStats.swift

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
}
}

Powered by TurnKey Linux.