oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import SwiftUI
3
4/// How this viewer has the call arranged: the layout, what is pinned, and
5/// the order of the people. One per `Call`, so the call window, the mini
6/// window and a rejoin all see the same arrangement; the order is kept for
7/// the meeting for as long as the app runs (`ParticipantOrders`).
8///
9/// Who is speaking is not here: it is measured with the levels
10/// (`AudioLevels.speaker`), and `stage(active:)` takes it as an input.
11@MainActor public final class CallArrangement: ObservableObject {
12 @Published public private(set) var layout: VideoLayout
13 /// What the person pinned to the stage in Speaker view. Nil follows the
14 /// speaker.
15 @Published public private(set) var pinned: StageItem?
16 @Published public private(set) var order = ParticipantOrder()
17 /// You, as the roster names you.
18 @Published public private(set) var me: String?
19 private var keyOf: [String: String] = [:]
20 private var peerOf: [String: String] = [:]
21 private var names: [String: String] = [:]
22 private(set) var meeting = ""
23 private let defaults: UserDefaults
24
25 public init(defaults: UserDefaults = .standard) {
26 self.defaults = defaults
27 layout = VideoLayout.load(defaults)
28 }
29
30 // MARK: Layout and pin
31
32 /// The person chose a layout: kept for the next call too.
33 public func choose(_ next: VideoLayout) {
34 guard next != layout else { return }
35 layout = next
36 next.save(defaults)
37 }
38
39 /// A layout for this call only (fixtures, tests).
40 @_spi(Fixtures) public func show(_ next: VideoLayout) { layout = next }
41
42 /// Pins `item` to the stage and says so; nil unpins. Pinning is a
43 /// Speaker view thing, so it switches to Speaker.
44 public func pin(_ item: StageItem?) {
45 guard item != pinned else { return }
46 pinned = item
47 if item != nil { choose(.speaker) }
48 AudioAnnouncer.say(item.map { StageWords.pinned(title(of: $0)) } ?? StageWords.unpinned)
49 }
50
51 /// Pins what is not pinned, and unpins what is.
52 public func togglePin(_ item: StageItem) { pin(pinned == item ? nil : item) }
53
54 /// Pinned without a word, for fixtures.
55 @_spi(Fixtures) public func showPinned(_ item: StageItem?) { pinned = item }
56
57 /// A person's tile held lifted, as if half way through a drag, for the
58 /// snapshots.
59 @Published public private(set) var fixtureLift: String?
60 @_spi(Fixtures) public func showLift(_ peer: String?) { fixtureLift = peer }
61 /// An order for the snapshots: `peers` first, in that order.
62 @_spi(Fixtures) public func showOrder(_ peers: [String]) {
63 for (index, peer) in peers.enumerated() { _ = change(peer, announce: false) { $0.move($1, to: index) } }
64 }
65
66 // MARK: The roster
67
68 /// The call started, for `meeting` (server and meeting id): puts back
69 /// the order kept for it, if any.
70 func begin(meeting: String) {
71 self.meeting = meeting
72 order = ParticipantOrders.load(meeting: meeting)
73 pinned = nil
74 }
75
76 /// Takes the roster as it is. Only while admitted: a roster while
77 /// reconnecting or waiting is not a statement about who has left.
78 func roster(_ state: RoomState?) {
79 guard let state else {
80 me = nil
81 pinned = nil
82 return
83 }
84 if me != state.peer { me = state.peer }
85 guard state.phase == "admitted" else { return }
86 let keyed = ParticipantKeys.keys(
87 roster: state.peers.map { ($0.peer, $0.name) }, me: state.peer)
88 keyOf = Dictionary(keyed.map { ($0.peer, $0.key) }, uniquingKeysWith: { a, _ in a })
89 peerOf = Dictionary(keyed.map { ($0.key, $0.peer) }, uniquingKeysWith: { a, _ in a })
90 names = Dictionary(state.peers.map { ($0.peer, $0.name) }, uniquingKeysWith: { a, _ in a })
91 var next = order
92 next.merge(present: keyed.map(\.key))
93 if next != order {
94 order = next
95 ParticipantOrders.save(order, meeting: meeting)
96 }
97 }
98
99 /// Everyone here, you included, in this viewer's order, as peer ids.
100 public var people: [String] { order.shown.compactMap { peerOf[$0] } }
101 /// Everyone but you, in order.
102 public var others: [String] { people.filter { $0 != me } }
103
104 // MARK: Moving people
105
106 /// Moves `peer` to `index` in `people`. Returns whether it moved.
107 @discardableResult
108 public func move(_ peer: String, to index: Int, announce: Bool = false) -> Bool {
109 change(peer, announce: announce) { $0.move($1, to: index) }
110 }
111 @discardableResult
112 public func moveEarlier(_ peer: String) -> Bool {
113 change(peer, announce: true) { $0.moveEarlier($1) }
114 }
115 @discardableResult
116 public func moveLater(_ peer: String) -> Bool {
117 change(peer, announce: true) { $0.moveLater($1) }
118 }
119 @discardableResult
120 public func moveToFirst(_ peer: String) -> Bool {
121 change(peer, announce: true) { $0.moveToFirst($1) }
122 }
123 public func canMove(_ peer: String) -> (earlier: Bool, later: Bool) {
124 guard let key = keyOf[peer] else { return (false, false) }
125 return order.canMove(key)
126 }
127
128 private func change(
129 _ peer: String, announce: Bool, _ step: (inout ParticipantOrder, String) -> Bool
130 ) -> Bool {
131 guard let key = keyOf[peer] else { return false }
132 var next = order
133 guard step(&next, key) else { return false }
134 order = next
135 ParticipantOrders.save(order, meeting: meeting)
136 if announce, let at = people.firstIndex(of: peer) {
137 AudioAnnouncer.say(StageWords.moved(name(of: peer), to: at, of: people.count))
138 }
139 return true
140 }
141
142 // MARK: The stage
143
144 /// What Speaker view's stage shows, with `active` the speaker.
145 public func stage(active: String?, screens: [String]) -> StageItem? {
146 Stage.speaker(pinned: pinned, active: active, others: others, screens: screens, me: me)
147 }
148
149 /// The filmstrip under that stage.
150 public func filmstrip(stage: StageItem?, screens: [String]) -> [StageItem] {
151 Stage.filmstrip(stage: stage, order: people, screens: screens)
152 }
153
154 func name(of peer: String) -> String { peer == me ? "You" : names[peer] ?? "Participant" }
155 func title(of item: StageItem) -> String {
156 switch item {
157 case .camera(let peer): peer == me ? "yourself" : name(of: peer)
158 case .screen(let peer): screenCaption(names[peer] ?? "Participant")
159 }
160 }
161}
162
163/// Who is speaking, observed alone: publishes only when the stage should
164/// move to someone else, a few times a minute in a lively call.
165@MainActor public final class SpeakerNow: ObservableObject {
166 @Published public private(set) var peer: String?
167 private var rule = ActiveSpeaker()
168
169 public init() {}
170
171 func update(_ voices: [ActiveSpeaker.Voice], present: Set<String>, now: Double) {
172 rule.update(voices, present: present, me: nil, now: now)
173 if peer != rule.current { peer = rule.current }
174 }
175
176 func reset() {
177 rule.reset()
178 if peer != nil { peer = nil }
179 }
180
181 /// Someone on the stage as the speaker, for the snapshots.
182 @_spi(Fixtures) public func showFixture(_ who: String?) { peer = who }
183}
184