import Foundation import os /// When "Reconnecting…" is said, and to whom (spec §15.23). /// /// Paul, 2026-09-30: "During that call I also saw 'Reconnecting' appear /// occasionally over the video, but only for a second … I didn't notice any /// interruption in the call." The core puts the call in its `reconnecting` /// phase the moment its signalling stream ends, and the stream ends on /// purpose every few minutes (its renewal) and is resumed half a second /// later; the media never notices. So the words wait until a problem has /// lasted: /// /// - the signalling stream down for `streamAfter` (4 s) — the whole call; /// - a connection `disconnected` or `failed` for `connectionAfter` (3 s), /// or no audio arriving from someone who is not muted for `audioAfter` /// (2 s) — that person's tile, "Reconnecting to Samuel…", unless it is /// everyone (two or more), which is the whole call again. /// /// Once said, the words stay `holdAfter` (1 s) past the recovery, so they /// do not flicker. Pure: fed the facts as they are, with the time. struct ReconnectWatch: Equatable { static let streamAfter = 4.0 static let connectionAfter = 3.0 static let audioAfter = 2.0 static let holdAfter = 1.0 /// One person's connection as it is now. struct Peer: Equatable { /// ICE `disconnected` or `failed`. var connectionBad = false /// No audio packets arrived at the last look, and they are not muted. var audioMissing = false } private var streamDownSince: Double? private var connectionBadSince: [String: Double] = [:] private var audioMissingSince: [String: Double] = [:] private var callClearSince: Double? private var peerClearSince: [String: Double] = [:] /// Whether the whole call is said to be reconnecting. private(set) var call = false /// The people whose tiles say "Reconnecting to …". private(set) var peers: Set = [] init() {} /// One look. `streamDown` is the core's `reconnecting` phase; `people` /// everyone in the call but you. mutating func observe(at now: Double, streamDown: Bool, people: [String: Peer]) { streamDownSince = streamDown ? (streamDownSince ?? now) : nil for id in Array(connectionBadSince.keys) where people[id] == nil { connectionBadSince.removeValue(forKey: id) } for id in Array(audioMissingSince.keys) where people[id] == nil { audioMissingSince.removeValue(forKey: id) } var troubled: Set = [] for (id, p) in people { connectionBadSince[id] = p.connectionBad ? (connectionBadSince[id] ?? now) : nil audioMissingSince[id] = p.audioMissing ? (audioMissingSince[id] ?? now) : nil let connection = connectionBadSince[id].map { now - $0 >= Self.connectionAfter } ?? false let audio = audioMissingSince[id].map { now - $0 >= Self.audioAfter } ?? false if connection || audio { troubled.insert(id) } } let stream = streamDownSince.map { now - $0 >= Self.streamAfter } ?? false let everyone = troubled.count >= 2 && troubled.count == people.count let wantCall = stream || everyone let wantPeers = wantCall ? [] : troubled if wantCall { call = true callClearSince = nil } else if call { let since = callClearSince ?? now callClearSince = since if now - since >= Self.holdAfter { call = false callClearSince = nil } } var shown = peers.filter { people[$0] != nil } for id in wantPeers { shown.insert(id) peerClearSince.removeValue(forKey: id) } for id in shown where !wantPeers.contains(id) { // Covered by the whole call's words, or better: held, then gone. if wantCall { shown.remove(id) peerClearSince.removeValue(forKey: id) continue } let since = peerClearSince[id] ?? now peerClearSince[id] = since if now - since >= Self.holdAfter { shown.remove(id) peerClearSince.removeValue(forKey: id) } } for id in Array(peerClearSince.keys) where !shown.contains(id) { peerClearSince.removeValue(forKey: id) } peers = shown } } /// The words (spec §15.23). public enum ReconnectWords { public static let call = "Reconnecting…" public static func peer(_ name: String) -> String { "Reconnecting to \(name)\u{2026}" } } /// `log show --predicate 'subsystem == "net.amadan.oficina.calls" && category == "connection"' --info`, /// and the last lines in "Copy call diagnostics": when "Reconnecting…" was /// said and cleared. enum CallLog { static let logger = Logger(subsystem: "net.amadan.oficina.calls", category: "connection") static func info(_ message: String) { logger.info("\(message, privacy: .public)") AudioDiagnosticsStore.connection.log("info", message) } } extension AudioDiagnosticsStore { /// The connection log's lines, kept apart from the others. public static let connection = AudioDiagnosticsStore() }