oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2
3/// "<name> is connecting and can't hear you yet" (spec §15.24, 2026-10-01).
4///
5/// Paul asked for Zoom's "… is connecting to audio and cannot hear you yet":
6/// the gap after someone joins, before their side is receiving and playing
7/// your audio, said rather than talked into. Their own report's `hearing`
8/// is the authority; a client that does not send one is judged on what this
9/// app can see of its own audio reaching them.
10///
11/// For each person from the moment they appear in the roster (or appear
12/// again, or are given a new connection):
13///
14/// - the words are said only once `grace` (1 s) has passed without their
15/// hearing you, so an ordinary join says nothing;
16/// - they go the moment their report lists you in `hearing` — "Samuel can
17/// hear you now";
18/// - for a client that does not say who it hears, once the connection is up
19/// and either their receiver reports show your audio reaching them or
20/// they have made a report without the list;
21/// - and never past `cap` (20 s), after which §15.20's and §15.23's words
22/// speak for any problem.
23///
24/// Your own mute does not matter: it is about their hearing you when you
25/// speak. Pure: fed the facts as they are, with the time. The browser's
26/// `connectingStep` is its twin.
27struct ConnectingWatch: Equatable {
28 static let grace = 1.0
29 static let cap = 20.0
30
31 /// One person as they are now.
32 struct Peer: Equatable {
33 /// The pair generation: a new one is a new connection, and they cannot
34 /// hear you on it yet.
35 var epoch: Int64 = 0
36 /// Their report on you (`HearingRule.hearsYou`): listed, not listed,
37 /// or nil for no list at all.
38 var hearsMe: Bool?
39 /// They have made a media report.
40 var reported = false
41 /// The connection to them is up.
42 var connected = false
43 /// Their receiver reports say your audio reaches them.
44 var acked = false
45 }
46
47 private struct Record: Equatable {
48 var epoch: Int64
49 var since: Double
50 var done = false
51 var shown = false
52 }
53 private var records: [String: Record] = [:]
54
55 init() {}
56
57 /// People still in their first moments, whether or not the words show yet.
58 var joining: Set<String> { Set(records.filter { !$0.value.done }.keys) }
59 /// People whose tiles say it.
60 var shown: Set<String> { Set(records.filter { $0.value.shown }.keys) }
61
62 /// One look at everyone but you. Returns the people whose words have just
63 /// gone because they can hear you, for "… can hear you now".
64 @discardableResult
65 mutating func observe(at now: Double, people: [String: Peer]) -> [String] {
66 for id in Array(records.keys) where people[id] == nil { records.removeValue(forKey: id) }
67 var cleared: [String] = []
68 for (id, p) in people {
69 var r = records[id] ?? Record(epoch: p.epoch, since: now)
70 if r.epoch != p.epoch { r = Record(epoch: p.epoch, since: now) }
71 if !r.done {
72 let fallback = p.hearsMe == nil && p.connected && (p.acked || p.reported)
73 if p.hearsMe == true || fallback {
74 if r.shown { cleared.append(id) }
75 r.done = true
76 r.shown = false
77 } else if now - r.since >= Self.cap {
78 r.done = true
79 r.shown = false
80 } else {
81 r.shown = now - r.since >= Self.grace
82 }
83 }
84 records[id] = r
85 }
86 return cleared.sorted()
87 }
88
89 /// What a person's tile says about them and you, in order (the browser's
90 /// `peerNotice`): in their first moments, their own more specific report
91 /// (their browser blocking sound, or no sound from you) and otherwise the
92 /// connecting words once the grace has passed — "Reconnecting" would be
93 /// wrong of a connection that has never been up; after that, §15.23's
94 /// "Reconnecting to …" first and then their reports. `connecting` is the
95 /// line that carries the moving ellipsis, and is never set beside
96 /// `notices`.
97 static func tile(
98 name: String, media: PeerMedia?, me: String, muted: Bool, joining: Bool, shown: Bool,
99 reconnecting: Bool
100 ) -> (notices: [String], connecting: String?) {
101 let reports = AudioWords.notices(name: name, media: media, me: me, muted: muted)
102 if joining {
103 if !reports.isEmpty { return (reports, nil) }
104 return ([], shown ? ConnectingWords.connecting(name) : nil)
105 }
106 return ((reconnecting ? [ReconnectWords.peer(name)] : []) + reports, nil)
107 }
108}
109
110/// This app's half of §15.24: whose audio it can say it hears.
111enum HearingRule {
112 /// One connection's audio as it is now.
113 struct Facts: Equatable {
114 /// ICE connected or completed.
115 var connected = false
116 /// ICE failed or closed: the path is gone.
117 var bad = false
118 var hasReceiver = false
119 /// The audio receiver's track is live.
120 var receiverLive = false
121 /// Audio packets received on it so far.
122 var packets: UInt64 = 0
123 /// Decoded audio frames that reached the receive tap since the last look.
124 var decoded = 0
125 /// They say they are muted (or have no microphone).
126 var senderMuted = false
127 }
128
129 /// Whether the audio path from someone is established. Once it is, it
130 /// stays so until the connection fails or closes. Established is: the
131 /// connection up, and either their audio arriving — packets received and
132 /// decoded frames coming out — on a live receiver, or, for someone who
133 /// says they are muted, nothing more than that: a muted sender may send
134 /// only silence, or no audio at all, and must not look unheard, because
135 /// the moment they unmute, it plays. The browser's `judgeHeard` is its
136 /// twin. A native app plays whatever it decodes: there is no autoplay
137 /// policy to refuse it.
138 static func heard(was: Bool, _ f: Facts) -> Bool {
139 if f.bad { return false }
140 if was { return true }
141 if !f.connected { return false }
142 if f.senderMuted, !f.hasReceiver || f.receiverLive { return true }
143 return f.receiverLive && f.packets > 0 && f.decoded > 0
144 }
145
146 /// Whether `hearing` has changed in a way worth a report: someone heard
147 /// who was not, or someone still here no longer heard. Someone who has
148 /// left is not news — every departure would otherwise cost a report from
149 /// everyone left — and the core carries their id, ignored, until the next
150 /// report for another reason. The browser's `hearingMoved` is its twin.
151 static func moved(from before: [String], to now: [String], present: Set<String>) -> Bool {
152 let was = Set(before)
153 let is_ = Set(now)
154 if !is_.subtracting(was).isEmpty { return true }
155 return !was.subtracting(is_).intersection(present).isEmpty
156 }
157
158 /// A person's report on you: true when their `hearing` lists you, false
159 /// when it has a list and you are not on it, nil when it has no list — no
160 /// report yet, or a client from before §15.24.
161 static func hearsYou(_ media: PeerMedia?, me: String) -> Bool? {
162 guard let list = media?.hearing else { return nil }
163 return !me.isEmpty && list.contains(me)
164 }
165}
166
167/// The words (spec §15.24). The ellipsis is drawn apart from them
168/// (`ConnectingText`), so VoiceOver hears the sentence and not the dots.
169public enum ConnectingWords {
170 public static func connecting(_ name: String) -> String { "\(name) is connecting and can’t hear you yet" }
171 public static func canHear(_ name: String) -> String { "\(name) can hear you now" }
172}
173