oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package protocol defines Calls’s client-neutral signaling contract.
2package protocol
3
4import "encoding/json"
5
6// PeerID is the server-issued identity of one participation: sixteen
7// random bytes in hex, bound to a room and a session, shown in rosters
8// so browsers can address each other. It is not a credential (spec §4.2).
9type PeerID string
10
11// Kind is a participation's authority, re-derived from standing on
12// every tick (spec §2.4).
13type Kind int
14
15// Reason says why a stream ended. Only "buffer" is followed by a
16// resume; the rest are terminal (spec §4.5).
17type Reason string
18
19const (
20 KindGuest Kind = iota
21 KindMember
22)
23
24func (k Kind) String() string {
25 if k == KindMember {
26 return "member"
27 }
28 return "guest"
29}
30
31const (
32 ReasonBuffer Reason = "buffer"
33 ReasonReplaced Reason = "replaced"
34 ReasonStanding Reason = "standing"
35 ReasonExpired Reason = "expired"
36 ReasonRemoved Reason = "removed"
37 ReasonEnded Reason = "ended"
38)
39
40// Upstream types a browser may send.
41const (
42 TypeJoin = "join"
43 TypeAdmit = "admit"
44 TypeLeave = "leave"
45 TypeOffer = "offer"
46 TypeAnswer = "answer"
47 TypeCandidate = "candidate"
48 TypeReset = "reset"
49 TypeChat = "chat" // slice 4
50 TypeShare = "share" // slice 4
51 // TypeMedia goes both ways: a peer reports its own MediaState
52 // upstream, and the hub fans the change out downstream as
53 // {type, from, name, payload: MediaState} (spec §15.19).
54 TypeMedia = "media"
55)
56
57// Downstream types the server emits.
58const (
59 TypeHello = "hello"
60 TypeRoster = "roster"
61 TypePeer = "peer"
62 TypeGone = "gone"
63 TypeKnock = "knock"
64 TypeKnockGone = "knock-gone"
65 TypeFull = "full"
66 // TypeEnded names the ending in §4.3's table, but nothing sends it:
67 // a meeting that ends closes every stream, and a closing stream
68 // carries its reason in lost, so the browser reads the ending as
69 // lost{reason: "ended"} (§4.5). The name is kept because the spec
70 // uses it.
71 TypeEnded = "ended"
72 TypeLost = "lost"
73 TypePair = "pair"
74 TypeGap = "gap"
75 TypeState = "state"
76)
77
78// Message is the one envelope, built by the server. Payload is opaque.
79type Message struct {
80 Type string `json:"type"`
81 From PeerID `json:"from,omitempty"`
82 Name string `json:"name,omitempty"`
83 To PeerID `json:"to,omitempty"`
84 Seq uint64 `json:"seq,omitempty"`
85 Gen uint64 `json:"gen,omitempty"`
86 Payload json.RawMessage `json:"payload,omitempty"`
87 Reason Reason `json:"reason,omitempty"`
88 Roster *Roster `json:"roster,omitempty"`
89 Boot string `json:"boot,omitempty"`
90 Peer PeerID `json:"peer,omitempty"`
91 Waiting bool `json:"waiting,omitempty"`
92 // Report is hello's report token (spec §15.26): the client's authority
93 // to send quality and problem reports, for its participation and ten
94 // minutes after. Sent back in the X-Calls-Report header, never logged.
95 Report string `json:"report,omitempty"`
96}
97
98// Roster is the authoritative snapshot a browser reconciles against
99// (spec §4.3). Waiting is filled only for a member recipient.
100type Roster struct {
101 Peers []RosterPeer `json:"peers"`
102 Waiting []RosterPeer `json:"waiting,omitempty"`
103}
104
105type RosterPeer struct {
106 Peer PeerID `json:"peer"`
107 Name string `json:"name"`
108 Kind string `json:"kind"`
109 Gen uint64 `json:"gen,omitempty"`
110 // Media is the peer's own last report of its microphone and camera,
111 // absent until it has made one — which is every client that predates
112 // §15.19, and every peer for the moment between admission and its
113 // first report. Absent means unknown, and a client shows unknown as
114 // neither muted nor camera-off. Never written through: a snapshot
115 // copies the pointer, so a change replaces it.
116 Media *MediaState `json:"media,omitempty"`
117}
118
119// MediaState is what a peer says about its own outgoing media (spec
120// §15.19): whether its microphone is muted and whether its camera is
121// off. It is a claim about what the peer is choosing to send, not a
122// measurement of what arrives — the server never sees media.
123type MediaState struct {
124 Muted bool `json:"muted"`
125 CameraOff bool `json:"cameraOff"`
126 // AudioBlocked and NoSoundFrom are the receiving half (spec §15.20):
127 // what this peer can say about the sound reaching it, so that the
128 // person talking can be told. Both carry only bad news and are
129 // omitted when there is none, so an ordinary report is the two-key
130 // object §15.19 defined and a server from before §15.20 still takes
131 // it.
132 //
133 // AudioBlocked: the browser refused to play remote sound (an autoplay
134 // policy wanting a click first).
135 AudioBlocked bool `json:"audioBlocked,omitempty"`
136 // NoSoundFrom: the peers whose audio is arriving here as silence —
137 // connected, reporting themselves unmuted, and flat for several
138 // seconds. At most MaxNoSoundFrom, each a peer id, none repeated.
139 NoSoundFrom []PeerID `json:"noSoundFrom,omitempty"`
140 // ScreenStalled (2026-09-28): the peers whose shared screen has stopped
141 // moving here while they say they are sharing — no frame decoded for
142 // several seconds. The sharer reads its own id in it as a request to
143 // recover (a keyframe first). Bad news only, omitted when there is
144 // none, at most MaxScreenStalled, each a peer id, none repeated.
145 ScreenStalled []PeerID `json:"screenStalled,omitempty"`
146 // Hearing (spec §15.24, 2026-10-01): the peers whose audio this client
147 // is actually receiving and playing — the path from them is
148 // established, so they can be heard the moment they speak. Unlike the
149 // members above it is good news, and its presence is itself news: nil
150 // (the key absent) is a client that does not say, an old one, and an
151 // empty list is a client that says it hears nobody yet. So it is
152 // omitted only when nil (omitzero), and an empty list travels as [].
153 // At most MaxHearing, each a peer id, none repeated.
154 Hearing []PeerID `json:"hearing,omitzero"`
155}
156
157// MaxNoSoundFrom bounds MediaState.NoSoundFrom: a room holds at most
158// sixteen participations (§4.2), so fifteen others is every one there
159// could be.
160const MaxNoSoundFrom = 15
161
162// MaxScreenStalled bounds MediaState.ScreenStalled for the same reason.
163const MaxScreenStalled = 15
164
165// MaxHearing bounds MediaState.Hearing for the same reason.
166const MaxHearing = 15
167
168// Equal compares two reports field by field; the peer lists are compared
169// in order, which is fine because the hub only asks whether a report
170// changed, and a reordered list is a report worth passing on.
171func (m MediaState) Equal(o MediaState) bool {
172 if m.Muted != o.Muted || m.CameraOff != o.CameraOff || m.AudioBlocked != o.AudioBlocked {
173 return false
174 }
175 if (m.Hearing == nil) != (o.Hearing == nil) {
176 return false
177 }
178 return samePeers(m.NoSoundFrom, o.NoSoundFrom) && samePeers(m.ScreenStalled, o.ScreenStalled) &&
179 samePeers(m.Hearing, o.Hearing)
180}
181
182func samePeers(a, b []PeerID) bool {
183 if len(a) != len(b) {
184 return false
185 }
186 for i := range a {
187 if a[i] != b[i] {
188 return false
189 }
190 }
191 return true
192}
193