fix: keep static NDI senders alive, count first stats interval

Senders like NDI Test Patterns send a still image once; a held NDI connection
now counts as alive so the receiver does not reconnect in a loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
Samuel Zielke 2 days ago
parent 37f46791e4
commit 28a81b6012

@ -29,7 +29,7 @@ final class StatsRecorder {
var lines = "" var lines = ""
for output in outputs { for output in outputs {
let s = output.snapshot let s = output.snapshot
let previous = lastReceived[output.displayID] ?? s.stats.received let previous = lastReceived[output.displayID] ?? 0
lastReceived[output.displayID] = s.stats.received lastReceived[output.displayID] = s.stats.received
let shownFPS = Double(s.stats.received - previous) / elapsed let shownFPS = Double(s.stats.received - previous) / elapsed
let refresh = screens.first { $0.id == output.displayID }?.refreshRate ?? 0 let refresh = screens.first { $0.id == output.displayID }?.refreshRate ?? 0

@ -1,4 +1,7 @@
/// Decides when a receiver counts as stalled and when to try reconnecting. /// Decides when a receiver counts as stalled and when to try reconnecting.
///
/// Frames and a confirmed connection both count as signs of life: some senders (e.g. NDI Test Patterns)
/// send a still image only once, which is not an outage.
public struct ConnectionWatchdog: Sendable, Equatable { public struct ConnectionWatchdog: Sendable, Equatable {
public enum State: Equatable, Sendable { public enum State: Equatable, Sendable {
case connecting case connecting
@ -11,6 +14,7 @@ public struct ConnectionWatchdog: Sendable, Equatable {
public var retryInterval: Double public var retryInterval: Double
private var lastFrame: Double? private var lastFrame: Double?
private var lastAlive: Double?
private var lastRetry: Double? private var lastRetry: Double?
private let startedAt: Double private let startedAt: Double
@ -22,11 +26,17 @@ public struct ConnectionWatchdog: Sendable, Equatable {
public mutating func frameArrived(at now: Double) { public mutating func frameArrived(at now: Double) {
lastFrame = now lastFrame = now
connectionConfirmed(at: now)
}
/// The sender still holds a connection to us, even if no new frames arrive.
public mutating func connectionConfirmed(at now: Double) {
lastAlive = now
lastRetry = nil lastRetry = nil
} }
public func state(at now: Double) -> State { public func state(at now: Double) -> State {
let reference = lastFrame ?? startedAt let reference = lastAlive ?? startedAt
guard now - reference >= stallAfter else { return lastFrame == nil ? .connecting : .live } guard now - reference >= stallAfter else { return lastFrame == nil ? .connecting : .live }
let since = lastRetry ?? reference + stallAfter let since = lastRetry ?? reference + stallAfter
return .stalled(retryIn: max(0, retryInterval - (now - since))) return .stalled(retryIn: max(0, retryInterval - (now - since)))

@ -108,7 +108,11 @@ public final class NDIReceiver: @unchecked Sendable {
lastPerformanceCheck = now lastPerformanceCheck = now
var dropped = NDIlib_recv_performance_t() var dropped = NDIlib_recv_performance_t()
NDIlib_recv_get_performance(instance, nil, &dropped) NDIlib_recv_get_performance(instance, nil, &dropped)
lock.withLock { current.stats.updateDroppedByReceiver(dropped.video_frames) } let connected = NDIlib_recv_get_no_connections(instance) > 0
lock.withLock {
current.stats.updateDroppedByReceiver(dropped.video_frames)
if connected { watchdog.connectionConfirmed(at: now) }
}
} }
if lock.withLock({ watchdog.shouldRetry(at: now) }) { if lock.withLock({ watchdog.shouldRetry(at: now) }) {

@ -105,6 +105,21 @@ import Testing
#expect(dog.state(at: 6.5) == .live) #expect(dog.state(at: 6.5) == .live)
} }
@Test func stillImageSourceWithConnectionIsNotStalled() {
var dog = ConnectionWatchdog(now: 0)
dog.frameArrived(at: 0)
for second in 1...10 { dog.connectionConfirmed(at: Double(second)) }
#expect(dog.state(at: 10.5) == .live)
let retry = dog.shouldRetry(at: 10.5)
#expect(!retry)
}
@Test func connectionWithoutAnyFrameStaysConnecting() {
var dog = ConnectionWatchdog(now: 0)
dog.connectionConfirmed(at: 5)
#expect(dog.state(at: 5.5) == .connecting)
}
@Test func neverConnectedSourceAlsoRetries() { @Test func neverConnectedSourceAlsoRetries() {
var dog = ConnectionWatchdog(now: 0) var dog = ConnectionWatchdog(now: 0)
let retry5 = dog.shouldRetry(at: 5) let retry5 = dog.shouldRetry(at: 5)

Loading…
Cancel
Save

Powered by TurnKey Linux.