import Foundation /// " is connecting and can't hear you yet" (spec §15.24, 2026-10-01). /// /// Paul asked for Zoom's "… is connecting to audio and cannot hear you yet": /// the gap after someone joins, before their side is receiving and playing /// your audio, said rather than talked into. Their own report's `hearing` /// is the authority; a client that does not send one is judged on what this /// app can see of its own audio reaching them. /// /// For each person from the moment they appear in the roster (or appear /// again, or are given a new connection): /// /// - the words are said only once `grace` (1 s) has passed without their /// hearing you, so an ordinary join says nothing; /// - they go the moment their report lists you in `hearing` — "Samuel can /// hear you now"; /// - for a client that does not say who it hears, once the connection is up /// and either their receiver reports show your audio reaching them or /// they have made a report without the list; /// - and never past `cap` (20 s), after which §15.20's and §15.23's words /// speak for any problem. /// /// Your own mute does not matter: it is about their hearing you when you /// speak. Pure: fed the facts as they are, with the time. The browser's /// `connectingStep` is its twin. struct ConnectingWatch: Equatable { static let grace = 1.0 static let cap = 20.0 /// One person as they are now. struct Peer: Equatable { /// The pair generation: a new one is a new connection, and they cannot /// hear you on it yet. var epoch: Int64 = 0 /// Their report on you (`HearingRule.hearsYou`): listed, not listed, /// or nil for no list at all. var hearsMe: Bool? /// They have made a media report. var reported = false /// The connection to them is up. var connected = false /// Their receiver reports say your audio reaches them. var acked = false } private struct Record: Equatable { var epoch: Int64 var since: Double var done = false var shown = false } private var records: [String: Record] = [:] init() {} /// People still in their first moments, whether or not the words show yet. var joining: Set { Set(records.filter { !$0.value.done }.keys) } /// People whose tiles say it. var shown: Set { Set(records.filter { $0.value.shown }.keys) } /// One look at everyone but you. Returns the people whose words have just /// gone because they can hear you, for "… can hear you now". @discardableResult mutating func observe(at now: Double, people: [String: Peer]) -> [String] { for id in Array(records.keys) where people[id] == nil { records.removeValue(forKey: id) } var cleared: [String] = [] for (id, p) in people { var r = records[id] ?? Record(epoch: p.epoch, since: now) if r.epoch != p.epoch { r = Record(epoch: p.epoch, since: now) } if !r.done { let fallback = p.hearsMe == nil && p.connected && (p.acked || p.reported) if p.hearsMe == true || fallback { if r.shown { cleared.append(id) } r.done = true r.shown = false } else if now - r.since >= Self.cap { r.done = true r.shown = false } else { r.shown = now - r.since >= Self.grace } } records[id] = r } return cleared.sorted() } /// What a person's tile says about them and you, in order (the browser's /// `peerNotice`): in their first moments, their own more specific report /// (their browser blocking sound, or no sound from you) and otherwise the /// connecting words once the grace has passed — "Reconnecting" would be /// wrong of a connection that has never been up; after that, §15.23's /// "Reconnecting to …" first and then their reports. `connecting` is the /// line that carries the moving ellipsis, and is never set beside /// `notices`. static func tile( name: String, media: PeerMedia?, me: String, muted: Bool, joining: Bool, shown: Bool, reconnecting: Bool ) -> (notices: [String], connecting: String?) { let reports = AudioWords.notices(name: name, media: media, me: me, muted: muted) if joining { if !reports.isEmpty { return (reports, nil) } return ([], shown ? ConnectingWords.connecting(name) : nil) } return ((reconnecting ? [ReconnectWords.peer(name)] : []) + reports, nil) } } /// This app's half of §15.24: whose audio it can say it hears. enum HearingRule { /// One connection's audio as it is now. struct Facts: Equatable { /// ICE connected or completed. var connected = false /// ICE failed or closed: the path is gone. var bad = false var hasReceiver = false /// The audio receiver's track is live. var receiverLive = false /// Audio packets received on it so far. var packets: UInt64 = 0 /// Decoded audio frames that reached the receive tap since the last look. var decoded = 0 /// They say they are muted (or have no microphone). var senderMuted = false } /// Whether the audio path from someone is established. Once it is, it /// stays so until the connection fails or closes. Established is: the /// connection up, and either their audio arriving — packets received and /// decoded frames coming out — on a live receiver, or, for someone who /// says they are muted, nothing more than that: a muted sender may send /// only silence, or no audio at all, and must not look unheard, because /// the moment they unmute, it plays. The browser's `judgeHeard` is its /// twin. A native app plays whatever it decodes: there is no autoplay /// policy to refuse it. static func heard(was: Bool, _ f: Facts) -> Bool { if f.bad { return false } if was { return true } if !f.connected { return false } if f.senderMuted, !f.hasReceiver || f.receiverLive { return true } return f.receiverLive && f.packets > 0 && f.decoded > 0 } /// Whether `hearing` has changed in a way worth a report: someone heard /// who was not, or someone still here no longer heard. Someone who has /// left is not news — every departure would otherwise cost a report from /// everyone left — and the core carries their id, ignored, until the next /// report for another reason. The browser's `hearingMoved` is its twin. static func moved(from before: [String], to now: [String], present: Set) -> Bool { let was = Set(before) let is_ = Set(now) if !is_.subtracting(was).isEmpty { return true } return !was.subtracting(is_).intersection(present).isEmpty } /// A person's report on you: true when their `hearing` lists you, false /// when it has a list and you are not on it, nil when it has no list — no /// report yet, or a client from before §15.24. static func hearsYou(_ media: PeerMedia?, me: String) -> Bool? { guard let list = media?.hearing else { return nil } return !me.isEmpty && list.contains(me) } } /// The words (spec §15.24). The ellipsis is drawn apart from them /// (`ConnectingText`), so VoiceOver hears the sentence and not the dots. public enum ConnectingWords { public static func connecting(_ name: String) -> String { "\(name) is connecting and can’t hear you yet" } public static func canHear(_ name: String) -> String { "\(name) can hear you now" } }