| 1 | import Foundation |
| 2 | |
| 3 | public struct RosterPeer: Decodable, Identifiable { |
| 4 | public let peer: String |
| 5 | public let name: String |
| 6 | public let kind: String |
| 7 | public let gen: Int64? |
| 8 | /// The peer's own last report of its microphone and camera (spec |
| 9 | /// §15.19). Nil is unknown — a client that predates the report, or one |
| 10 | /// that has not made it yet — and is shown as neither muted nor off. |
| 11 | public let media: PeerMedia? |
| 12 | public var id: String { peer } |
| 13 | } |
| 14 | /// What a peer says about its own outgoing media (spec §15.19), and about |
| 15 | /// the sound reaching it (§15.20). |
| 16 | public struct PeerMedia: Decodable, Equatable, Sendable { |
| 17 | public let muted: Bool |
| 18 | public let cameraOff: Bool |
| 19 | /// Its browser refused to play sound and is waiting for a click. Nil |
| 20 | /// (absent) is none; the key only travels with bad news. |
| 21 | public let audioBlocked: Bool? |
| 22 | /// The peers whose audio reaches it as silence. Nil (absent) is none. |
| 23 | public let noSoundFrom: [String]? |
| 24 | /// The peers whose shared screen has stopped moving for it (§15.21). |
| 25 | /// A sharer that finds itself here recovers its screen. Nil is none. |
| 26 | public let screenStalled: [String]? |
| 27 | /// The peers whose audio it is actually receiving and playing (§15.24). |
| 28 | /// Nil (absent) is a client that does not say; empty is one that hears |
| 29 | /// nobody yet. |
| 30 | public let hearing: [String]? |
| 31 | |
| 32 | public init( |
| 33 | muted: Bool, cameraOff: Bool, audioBlocked: Bool? = nil, noSoundFrom: [String]? = nil, |
| 34 | screenStalled: [String]? = nil, hearing: [String]? = nil |
| 35 | ) { |
| 36 | self.muted = muted |
| 37 | self.cameraOff = cameraOff |
| 38 | self.audioBlocked = audioBlocked |
| 39 | self.noSoundFrom = noSoundFrom |
| 40 | self.screenStalled = screenStalled |
| 41 | self.hearing = hearing |
| 42 | } |
| 43 | } |
| 44 | public struct RoomState: Decodable { |
| 45 | public let phase: String |
| 46 | public let peer: String |
| 47 | public let boot: String |
| 48 | public let reason: String? |
| 49 | public let peers: [RosterPeer] |
| 50 | public let waiting: [RosterPeer] |
| 51 | } |
| 52 | struct WireEvent: Decodable { |
| 53 | let type: String |
| 54 | let state: RoomState? |
| 55 | let message: Message? |
| 56 | let ice: ICEConfig? |
| 57 | let info: MeetingInfo? |
| 58 | let signal: Message? |
| 59 | } |
| 60 | struct Message: Decodable { |
| 61 | let type: String |
| 62 | let from: String? |
| 63 | let peer: String? |
| 64 | let name: String? |
| 65 | let gen: Int64? |
| 66 | let seq: UInt64? |
| 67 | let payload: Payload? |
| 68 | } |
| 69 | struct Payload: Decodable { |
| 70 | let description: Description? |
| 71 | let candidate: Candidate? |
| 72 | let text: String? |
| 73 | /// `share`'s one field (§5.4): whether the sender is showing a screen. |
| 74 | let on: Bool? |
| 75 | } |
| 76 | struct Description: Codable { |
| 77 | let type: String |
| 78 | let sdp: String |
| 79 | } |
| 80 | struct Candidate: Codable { |
| 81 | let candidate: String |
| 82 | let sdpMid: String? |
| 83 | let sdpMLineIndex: Int32? |
| 84 | } |
| 85 | struct ICEConfig: Decodable { |
| 86 | struct Server: Decodable { |
| 87 | let urls: [String] |
| 88 | let username: String? |
| 89 | let credential: String? |
| 90 | } |
| 91 | let iceServers: [Server] |
| 92 | let renewAt: Int64 |
| 93 | let expiresAt: Int64 |
| 94 | } |
| 95 | /// §5.3 for the apps: when a fresh `/ice` answer should restart ICE on a |
| 96 | /// connection, rather than be adopted for whatever it gathers next. |
| 97 | /// |
| 98 | /// The core fetches the list on every roster (every stream resume, five |
| 99 | /// minutes apart at most) as well as at `renewAt`, so a connection hears a |
| 100 | /// fresh list at the latest five minutes after its own renewal falls due — |
| 101 | /// well inside the half hour between `renewAt` and expiry. |
| 102 | enum ICERenewal { |
| 103 | /// - Parameters: |
| 104 | /// - gathered: the list the connection was built with, or last restarted onto. |
| 105 | /// - next: the list just fetched. |
| 106 | /// - now: unix seconds. |
| 107 | static func restart(gathered: ICEConfig?, next: ICEConfig, now: Int64) -> Bool { |
| 108 | // The floor has nothing to renew onto. |
| 109 | guard !next.iceServers.isEmpty else { return false } |
| 110 | guard let gathered else { return true } |
| 111 | // Built on the floor while the first fetch was out, and a relay since: |
| 112 | // adopt it now, as the browser's adoptServers does. |
| 113 | if gathered.iceServers.isEmpty { return true } |
| 114 | return gathered.renewAt > 0 && now >= gathered.renewAt |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | func json(_ value: Any) -> String? { |
| 119 | guard JSONSerialization.isValidJSONObject(value), |
| 120 | let data = try? JSONSerialization.data(withJSONObject: value) |
| 121 | else { return nil } |
| 122 | return String(data: data, encoding: .utf8) |
| 123 | } |
| 124 | |
| 125 | struct MeetingInfo: Decodable { |
| 126 | let title: String |
| 127 | /// A title someone chose rather than one Calls wrote. Absent from a |
| 128 | /// server before 0.4.7, where it reads as false and no title is shown. |
| 129 | let named: Bool? |
| 130 | let name: String |
| 131 | let standing: String |
| 132 | let floor: Bool |
| 133 | } |
| 134 | |