oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1package callscore
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "net/http"
8 "net/url"
9 "sort"
10 "sync"
11 "time"
12
13 "amadan.net/oficina/calls/protocol"
14)
15
16// Event crosses the native bridge as JSON. Message is a server envelope;
17// snapshot events carry authoritative local state. Media never crosses it.
18type Event struct {
19 Info *Info `json:"info,omitempty"`
20 Type string `json:"type"`
21 State *Snapshot `json:"state,omitempty"`
22 Message *protocol.Message `json:"message,omitempty"`
23 ICE *ICEServers `json:"ice,omitempty"`
24 Signal *protocol.Signal `json:"signal,omitempty"`
25}
26type Snapshot struct {
27 Phase string `json:"phase"`
28 Peer protocol.PeerID `json:"peer"`
29 Boot string `json:"boot"`
30 Reason string `json:"reason,omitempty"`
31 Peers []protocol.RosterPeer `json:"peers"`
32 Waiting []protocol.RosterPeer `json:"waiting"`
33}
34
35type lane struct {
36 queue chan protocol.Signal
37 ctx context.Context
38 cancel context.CancelFunc
39}
40
41// Room is single-use. Callbacks are serialized and must return promptly. A host
42// should enqueue them on its UI executor, never perform media work inline.
43type Room struct {
44 mu sync.Mutex
45 emitMu sync.Mutex
46 t *transport
47 callback func(Event)
48 ctx context.Context
49 cancel context.CancelFunc
50 wg sync.WaitGroup
51 started bool
52 stopped bool
53 state Snapshot
54 last map[protocol.PeerID]uint64
55 seq map[protocol.PeerID]uint64
56 lanes map[protocol.PeerID]*lane
57 blocked map[protocol.PeerID]bool
58 iceWake chan struct{}
59 // media is this client's own microphone and camera, as the host last
60 // set it (§15.19). It outlives a fresh join: a restart does not
61 // unmute anybody.
62 media protocol.MediaState
63 // plainScreen: the server refused a report naming stalled screens
64 // (it predates §15.21), so reports go without them.
65 plainScreen bool
66 // plainHearing: the server refused a report saying who this client
67 // hears (it predates §15.24), so reports go without the key, and the
68 // people on new clients fall back to their own evidence about us.
69 plainHearing bool
70 // report is the latest hello's report token (spec §15.26): a resumed
71 // hello carries the same one, a fresh participation a new one. Kept
72 // past Close, for the after-call window (report.go).
73 report string
74}
75
76func New(origin, room string, authenticated *http.Client, callback func(Event)) (*Room, error) {
77 t, err := newTransport(origin, room, authenticated)
78 if err != nil {
79 return nil, err
80 }
81 if callback == nil {
82 return nil, errors.New("a room event handler is required")
83 }
84 ctx, cancel := context.WithCancel(context.Background())
85 return &Room{t: t, callback: callback, ctx: ctx, cancel: cancel, state: Snapshot{Phase: "ready", Peers: []protocol.RosterPeer{}, Waiting: []protocol.RosterPeer{}}, media: protocol.MediaState{Hearing: []protocol.PeerID{}}, last: map[protocol.PeerID]uint64{}, seq: map[protocol.PeerID]uint64{}, lanes: map[protocol.PeerID]*lane{}, blocked: map[protocol.PeerID]bool{}, iceWake: make(chan struct{}, 1)}, nil
86}
87func (r *Room) emit(e Event) { r.emitMu.Lock(); defer r.emitMu.Unlock(); r.callback(e) }
88func (r *Room) snapshotLocked() Snapshot {
89 s := r.state
90 s.Peers = append([]protocol.RosterPeer{}, s.Peers...)
91 s.Waiting = append([]protocol.RosterPeer{}, s.Waiting...)
92 return s
93}
94func (r *Room) Snapshot() Snapshot { r.mu.Lock(); defer r.mu.Unlock(); return r.snapshotLocked() }
95
96// ShareLink asks the server for a current invitation using this session's
97// authority. It is blocking and belongs on the host's background executor.
98func (r *Room) ShareLink() (string, error) { return r.t.shareLink(r.ctx) }
99func (r *Room) publish() { s := r.Snapshot(); r.emit(Event{Type: "state", State: &s}) }
100func (r *Room) Start() error {
101 r.mu.Lock()
102 if r.started || r.stopped {
103 r.mu.Unlock()
104 return errors.New("room already started or closed")
105 }
106 r.started = true
107 r.state.Phase = "connecting"
108 r.wg.Add(2)
109 r.mu.Unlock()
110 r.publish()
111 go func() { defer r.wg.Done(); r.streamLoop() }()
112 go func() { defer r.wg.Done(); r.iceLoop() }()
113 return nil
114}
115
116// Close cancels all network work and waits for callbacks to finish. Do not call
117// it from a callback. Leave attempts the departure before closing.
118func (r *Room) Close() {
119 r.mu.Lock()
120 r.stopped = true
121 r.cancel()
122 r.mu.Unlock()
123 r.wg.Wait()
124}
125func (r *Room) Leave() {
126 r.mu.Lock()
127 peer := r.state.Peer
128 r.mu.Unlock()
129 if peer != "" {
130 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
131 _ = r.t.post(ctx, protocol.Signal{Peer: peer, Type: protocol.TypeLeave})
132 cancel()
133 }
134 r.terminal("left")
135 r.Close()
136}
137func (r *Room) terminal(reason string) {
138 r.mu.Lock()
139 if r.stopped {
140 r.mu.Unlock()
141 return
142 }
143 r.stopped = true
144 r.state.Phase = "ended"
145 r.state.Reason = reason
146 r.state.Peers = []protocol.RosterPeer{}
147 r.state.Waiting = []protocol.RosterPeer{}
148 r.cancel()
149 r.mu.Unlock()
150 r.publish()
151}
152func (r *Room) fresh() {
153 r.mu.Lock()
154 r.state = Snapshot{Phase: "connecting", Peers: []protocol.RosterPeer{}, Waiting: []protocol.RosterPeer{}}
155 r.last = map[protocol.PeerID]uint64{}
156 r.seq = map[protocol.PeerID]uint64{}
157 for _, l := range r.lanes {
158 l.cancel()
159 }
160 r.lanes = map[protocol.PeerID]*lane{}
161 r.blocked = map[protocol.PeerID]bool{}
162 r.mu.Unlock()
163 r.publish()
164}
165func (r *Room) streamLoop() {
166 if info, err := r.t.info(r.ctx); err == nil {
167 r.emit(Event{Type: "info", Info: &info})
168 }
169
170 delay := 500 * time.Millisecond
171 // A fresh open refused with 409 means this session already holds a
172 // participation in the room. For an app that is its own ghost: the
173 // previous run of the app (quit, crashed, relaunched by an update)
174 // whose participation is still inside its thirty-second resume
175 // window. The session is the app's alone, so it takes its own place
176 // back, once, rather than ending the call as "open elsewhere" before
177 // it began. A second instance that really is elsewhere is told
178 // lost{replaced} on its own stream, and does not take it back.
179 tookOver := false
180 for r.ctx.Err() == nil {
181 r.mu.Lock()
182 peer := r.state.Peer
183 q := url.Values{}
184 if peer != "" {
185 b, _ := json.Marshal(r.last)
186 q.Set("peer", string(peer))
187 q.Set("last", string(b))
188 }
189 r.mu.Unlock()
190 began := time.Now()
191 streamCtx, streamCancel := context.WithTimeout(r.ctx, 6*time.Minute)
192 res, err := r.t.request(streamCtx, http.MethodGet, "stream", q, nil)
193 if err == nil {
194 err = readEvents(res.Body, r.receive)
195 res.Body.Close()
196 }
197 streamCancel()
198 if r.ctx.Err() != nil {
199 return
200 }
201 var refusal *HTTPError
202 if errors.As(err, &refusal) {
203 switch refusal.Status {
204 case 410:
205 reason := refusal.Reason
206 if reason == "" {
207 reason = "expired"
208 }
209 r.terminal(reason)
210 return
211 case 409:
212 if peer == "" && !tookOver {
213 tookOver = true
214 if r.t.takeover(r.ctx) == nil {
215 continue
216 }
217 }
218 r.terminal("replaced")
219 return
220 case 401, 403, 302, 303, 307, 308:
221 r.terminal("signin")
222 return
223 case 404:
224 if peer != "" {
225 r.fresh()
226 delay = 500 * time.Millisecond
227 continue
228 }
229 r.terminal("unavailable")
230 return
231 case 400, 406:
232 r.terminal("protocol")
233 return
234 }
235 }
236 r.mu.Lock()
237 r.state.Phase = "reconnecting"
238 r.mu.Unlock()
239 r.publish()
240 if time.Since(began) > 5*time.Second {
241 delay = 500 * time.Millisecond
242 }
243 if !pause(r.ctx, delay) {
244 return
245 }
246 delay = min(delay*2, 10*time.Second)
247 }
248}
249func pause(ctx context.Context, d time.Duration) bool {
250 timer := time.NewTimer(d)
251 defer timer.Stop()
252 select {
253 case <-ctx.Done():
254 return false
255 case <-timer.C:
256 return true
257 }
258}
259
260// SetMedia records this client's own microphone and camera state and,
261// once admitted, reports it (§15.19). Before admission there is nobody
262// to tell; the roster that admits this client is compared against it
263// and the report goes then, as it does after every resume and restart.
264func (r *Room) SetMedia(muted, cameraOff bool) error {
265 r.mu.Lock()
266 if r.media.Muted == muted && r.media.CameraOff == cameraOff {
267 r.mu.Unlock()
268 return nil
269 }
270 r.media.Muted, r.media.CameraOff = muted, cameraOff
271 r.mu.Unlock()
272 return r.reportMedia()
273}
274
275// SetNoSoundFrom records which peers this client is receiving silence
276// from (spec §15.20) and reports it with the rest of its media state.
277// Only peers in the current roster other than this client are kept, in
278// roster order, so the report is stable and bounded; an empty list is
279// the ordinary report. A native app is never refused playback by an
280// autoplay policy, so audioBlocked is never set from here.
281func (r *Room) SetNoSoundFrom(peers []protocol.PeerID) error {
282 want := make(map[protocol.PeerID]bool, len(peers))
283 for _, p := range peers {
284 want[p] = true
285 }
286 r.mu.Lock()
287 var next []protocol.PeerID
288 for _, p := range r.state.Peers {
289 if p.Peer != r.state.Peer && want[p.Peer] && len(next) < protocol.MaxNoSoundFrom {
290 next = append(next, p.Peer)
291 }
292 }
293 if (protocol.MediaState{NoSoundFrom: next}).Equal(protocol.MediaState{NoSoundFrom: r.media.NoSoundFrom}) {
294 r.mu.Unlock()
295 return nil
296 }
297 r.media.NoSoundFrom = next
298 r.mu.Unlock()
299 return r.reportMedia()
300}
301
302// SetScreenStalled records whose shared screen has stopped moving here
303// (spec §15.21) and reports it with the rest of this client's media
304// state, exactly as SetNoSoundFrom does its list: roster peers only, in
305// roster order, bounded, and an empty list the ordinary report.
306func (r *Room) SetScreenStalled(peers []protocol.PeerID) error {
307 want := make(map[protocol.PeerID]bool, len(peers))
308 for _, p := range peers {
309 want[p] = true
310 }
311 r.mu.Lock()
312 var next []protocol.PeerID
313 for _, p := range r.state.Peers {
314 if p.Peer != r.state.Peer && want[p.Peer] && len(next) < protocol.MaxScreenStalled {
315 next = append(next, p.Peer)
316 }
317 }
318 if (protocol.MediaState{ScreenStalled: next}).Equal(protocol.MediaState{ScreenStalled: r.media.ScreenStalled}) {
319 r.mu.Unlock()
320 return nil
321 }
322 r.media.ScreenStalled = next
323 r.mu.Unlock()
324 return r.reportMedia()
325}
326
327// SetHearing records which peers this client is actually receiving and
328// playing audio from (spec §15.24) and reports it with the rest of its
329// media state. Roster peers only, in roster order, bounded, exactly as
330// SetNoSoundFrom keeps its list — except that empty is not absent here:
331// this client always says who it hears, and "nobody yet" is [] on the
332// wire, so the people it has not yet heard know to wait for it rather
333// than fall back to guessing.
334func (r *Room) SetHearing(peers []protocol.PeerID) error {
335 want := make(map[protocol.PeerID]bool, len(peers))
336 for _, p := range peers {
337 want[p] = true
338 }
339 r.mu.Lock()
340 next := []protocol.PeerID{}
341 for _, p := range r.state.Peers {
342 if p.Peer != r.state.Peer && want[p.Peer] && len(next) < protocol.MaxHearing {
343 next = append(next, p.Peer)
344 }
345 }
346 if (protocol.MediaState{Hearing: next}).Equal(protocol.MediaState{Hearing: r.media.Hearing}) {
347 r.mu.Unlock()
348 return nil
349 }
350 r.media.Hearing = next
351 r.mu.Unlock()
352 return r.reportMedia()
353}
354
355func (r *Room) reportMedia() error {
356 r.mu.Lock()
357 report := r.state.Phase == "admitted" && !r.stopped
358 r.mu.Unlock()
359 if !report {
360 return nil
361 }
362 return r.sendMedia()
363}
364
365func (r *Room) sendMedia() error {
366 r.mu.Lock()
367 b, err := json.Marshal(r.wireMediaLocked())
368 r.mu.Unlock()
369 if err != nil {
370 return err
371 }
372 return r.Send(protocol.TypeMedia, "", 0, string(b))
373}
374
375// wireMediaLocked is the report as this server will take it: the host's
376// state without the keys an older server has refused.
377func (r *Room) wireMediaLocked() protocol.MediaState {
378 report := r.media
379 if r.plainScreen {
380 // A server from before §15.21 refuses the key; everything else
381 // in the report still matters.
382 report.ScreenStalled = nil
383 }
384 if r.plainHearing {
385 report.Hearing = nil
386 }
387 return report
388}
389
390// refusedExtras is a server from before §15.24 or §15.21 refusing a media
391// report because of a key it does not know: the newest key goes first —
392// hearing, which every report from this core carries — and the report is
393// sent again without it; a second refusal, of a report naming stalled
394// screens, takes those off too. From then on reports go without them, so
395// a mute is never lost to an old server. Without hearing, the people on
396// new clients fall back to their own evidence that they can be heard
397// (§15.24); without screenStalled the sharer recovers on its own watchdog.
398func (r *Room) refusedExtras(s protocol.Signal) bool {
399 var sent protocol.MediaState
400 if json.Unmarshal(s.Payload, &sent) != nil {
401 return false
402 }
403 r.mu.Lock()
404 dropped := false
405 if sent.Hearing != nil && !r.plainHearing {
406 r.plainHearing = true
407 dropped = true
408 } else if len(sent.ScreenStalled) > 0 && !r.plainScreen {
409 r.plainScreen = true
410 dropped = true
411 }
412 r.mu.Unlock()
413 if dropped {
414 go func() { _ = r.sendMedia() }()
415 }
416 return dropped
417}
418
419// mediaStaleLocked says whether the hub's record of this client — its
420// own entry in the roster just applied — differs from what the host last
421// set, or is missing, which is how a fresh participation, a restarted
422// server, or a report lost while the stream was down all look.
423func (r *Room) mediaStaleLocked() bool {
424 for _, p := range r.state.Peers {
425 if p.Peer == r.state.Peer {
426 return p.Media == nil || !p.Media.Equal(r.wireMediaLocked())
427 }
428 }
429 return false
430}
431
432// applyMediaLocked is a `media` event: one peer's new state, replacing
433// its roster entry's pointer rather than writing through it, because
434// snapshots already handed out share the old one.
435func (r *Room) applyMediaLocked(m protocol.Message) {
436 var next protocol.MediaState
437 if m.From == "" || json.Unmarshal(m.Payload, &next) != nil {
438 return
439 }
440 for i, p := range r.state.Peers {
441 if p.Peer == m.From {
442 r.state.Peers[i].Media = &next
443 }
444 }
445}
446
447func (r *Room) generationLocked(peer protocol.PeerID) uint64 {
448 for _, p := range r.state.Peers {
449 if p.Peer == peer {
450 return p.Gen
451 }
452 }
453 return 0
454}
455func targeted(typ string) bool {
456 return typ == protocol.TypeOffer || typ == protocol.TypeAnswer || typ == protocol.TypeCandidate
457}
458
459func (r *Room) receive(m protocol.Message) error {
460 r.mu.Lock()
461 if r.stopped {
462 r.mu.Unlock()
463 return context.Canceled
464 }
465 join, ice, reset, report := false, false, protocol.PeerID(""), false
466 switch m.Type {
467 case protocol.TypeHello:
468 join = r.state.Peer == ""
469 r.state.Peer = m.Peer
470 r.state.Boot = m.Boot
471 if m.Report != "" {
472 r.report = m.Report
473 }
474 if join {
475 r.state.Phase = "joining"
476 }
477 case protocol.TypeRoster:
478 if m.Roster == nil {
479 r.mu.Unlock()
480 return errors.New("missing roster")
481 }
482 for _, p := range m.Roster.Peers {
483 if p.Gen > r.generationLocked(p.Peer) {
484 delete(r.blocked, p.Peer)
485 }
486 }
487 for _, p := range r.state.Peers {
488 present := false
489 for _, next := range m.Roster.Peers {
490 if next.Peer == p.Peer {
491 present = true
492 }
493 }
494 if !present {
495 r.dropLaneLocked(p.Peer)
496 }
497 }
498 r.state.Peers = append([]protocol.RosterPeer{}, m.Roster.Peers...)
499 r.state.Waiting = append([]protocol.RosterPeer{}, m.Roster.Waiting...)
500 r.state.Phase = "admitted"
501 ice = true
502 report = r.mediaStaleLocked()
503 case protocol.TypePeer:
504 if m.Roster != nil {
505 for _, p := range m.Roster.Peers {
506 r.upsertLocked(p)
507 }
508 }
509 case protocol.TypeGone:
510 r.state.Peers = without(r.state.Peers, m.From)
511 r.dropLaneLocked(m.From)
512 case protocol.TypeKnock:
513 r.state.Waiting = without(r.state.Waiting, m.From)
514 r.state.Waiting = append(r.state.Waiting, protocol.RosterPeer{Peer: m.From, Name: m.Name, Kind: "guest"})
515 case protocol.TypeKnockGone:
516 r.state.Waiting = without(r.state.Waiting, m.From)
517 case protocol.TypeState:
518 if m.Waiting {
519 r.state.Phase = "waiting"
520 }
521 case protocol.TypeFull:
522 if r.state.Phase != "admitted" {
523 r.state.Phase = "full"
524 }
525 case protocol.TypePair:
526 for i, p := range r.state.Peers {
527 if p.Peer == m.Peer && m.Gen > p.Gen {
528 r.state.Peers[i].Gen = m.Gen
529 delete(r.blocked, m.Peer)
530 }
531 }
532 case protocol.TypeMedia:
533 r.applyMediaLocked(m)
534 case protocol.TypeGap:
535 reset = m.From
536 case protocol.TypeLost:
537 r.mu.Unlock()
538 if m.Reason == protocol.ReasonBuffer {
539 return errors.New("resume required")
540 }
541 r.terminal(string(m.Reason))
542 return context.Canceled
543 default:
544 if targeted(m.Type) {
545 gen := r.generationLocked(m.From)
546 if m.Seq == 0 || m.Seq <= r.last[m.From] {
547 r.mu.Unlock()
548 return nil
549 }
550 r.last[m.From] = m.Seq
551 if gen == 0 || m.Gen < gen {
552 r.mu.Unlock()
553 return nil
554 }
555 if m.Gen > gen {
556 for i, p := range r.state.Peers {
557 if p.Peer == m.From {
558 r.state.Peers[i].Gen = m.Gen
559 }
560 }
561 }
562 }
563 }
564 r.mu.Unlock()
565 r.publish()
566 r.emit(Event{Type: "message", Message: &m})
567 if join {
568 if err := r.Send(protocol.TypeJoin, "", 0, ""); err != nil {
569 return err
570 }
571 }
572 if ice {
573 select {
574 case r.iceWake <- struct{}{}:
575 default:
576 }
577 }
578 if reset != "" {
579 _ = r.Send(protocol.TypeReset, string(reset), 0, "")
580 }
581 if report {
582 _ = r.sendMedia()
583 }
584 return nil
585}
586func without(peers []protocol.RosterPeer, id protocol.PeerID) []protocol.RosterPeer {
587 out := make([]protocol.RosterPeer, 0, len(peers))
588 for _, p := range peers {
589 if p.Peer != id {
590 out = append(out, p)
591 }
592 }
593 return out
594}
595func (r *Room) upsertLocked(p protocol.RosterPeer) {
596 r.state.Peers = without(r.state.Peers, p.Peer)
597 r.state.Peers = append(r.state.Peers, p)
598 sort.Slice(r.state.Peers, func(i, j int) bool { return r.state.Peers[i].Peer < r.state.Peers[j].Peer })
599}
600
601// Send enqueues without blocking the UI. expectedGen prevents a late native
602// media callback from being relabelled as part of a replacement connection.
603func (r *Room) Send(typ, to string, expectedGen uint64, payload string) error {
604 r.mu.Lock()
605 defer r.mu.Unlock()
606 if r.stopped || !r.started || r.state.Peer == "" {
607 return errors.New("room is not connected")
608 }
609 switch typ {
610 case protocol.TypeJoin, protocol.TypeAdmit, protocol.TypeReset, protocol.TypeChat, protocol.TypeShare, protocol.TypeMedia, protocol.TypeOffer, protocol.TypeAnswer, protocol.TypeCandidate:
611 default:
612 return errors.New("unsupported room command")
613 }
614 target := protocol.PeerID(to)
615 s := protocol.Signal{Peer: r.state.Peer, Type: typ, To: target}
616 if payload != "" {
617 if !json.Valid([]byte(payload)) {
618 return errors.New("invalid signal payload")
619 }
620 s.Payload = json.RawMessage(payload)
621 }
622 if targeted(typ) {
623 gen := r.generationLocked(target)
624 if target == r.state.Peer || gen == 0 || gen != expectedGen || r.blocked[target] {
625 return errors.New("peer connection is no longer current")
626 }
627 s.Gen = gen
628 s.Seq = r.seq[target] + 1
629 }
630 b, err := json.Marshal(s)
631 if err != nil || len(b) > 64*1024 {
632 return errors.New("signal exceeds Calls' message limit")
633 }
634 laneKey := target
635 if typ == protocol.TypeReset {
636 if r.generationLocked(target) == 0 {
637 return errors.New("peer is no longer present")
638 }
639 laneKey = ""
640 }
641 l := r.lanes[laneKey]
642 if l == nil {
643 if len(r.lanes) >= 17 {
644 return errors.New("too many signaling destinations")
645 }
646 ctx, cancel := context.WithCancel(r.ctx)
647 l = &lane{queue: make(chan protocol.Signal, 64), ctx: ctx, cancel: cancel}
648 r.lanes[laneKey] = l
649 r.wg.Add(1)
650 go func() { defer r.wg.Done(); r.sendLoop(l) }()
651 }
652 select {
653 case l.queue <- s:
654 if targeted(typ) {
655 r.seq[target] = s.Seq
656 }
657 return nil
658 default:
659 return errors.New("signaling queue is full")
660 }
661}
662func (r *Room) current(s protocol.Signal) bool {
663 r.mu.Lock()
664 defer r.mu.Unlock()
665 return !r.stopped && s.Peer == r.state.Peer && (!targeted(s.Type) || (r.generationLocked(s.To) == s.Gen && !r.blocked[s.To]))
666}
667func (r *Room) sendLoop(l *lane) {
668 // Cancellation removes this lane under mu before any new Send can use it.
669 // Report every accepted chat still waiting in its queue; the UI has already
670 // cleared the composer and must retain a visible failure instead of losing it.
671 defer func() {
672 for {
673 select {
674 case s := <-l.queue:
675 if s.Type == protocol.TypeChat {
676 r.emit(Event{Type: "send-failed", Signal: &s})
677 }
678 default:
679 return
680 }
681 }
682 }()
683 for {
684 if l.ctx.Err() != nil {
685 return
686 }
687 select {
688 case <-l.ctx.Done():
689 return
690 case s := <-l.queue:
691 ctx, cancel := context.WithTimeout(l.ctx, 30*time.Second)
692 delay := 500 * time.Millisecond
693 accepted := false
694 for r.current(s) {
695 err := r.t.post(ctx, s)
696 if err == nil {
697 accepted = true
698 break
699 }
700 var refusal *HTTPError
701 if errors.As(err, &refusal) {
702 if refusal.Status == 429 {
703 delay = max(delay, refusal.RetryAfter)
704 } else if refusal.Status < 500 {
705 if refusal.Status == 400 && s.Type == protocol.TypeMedia && r.refusedExtras(s) {
706 break
707 }
708 if refusal.Status == 401 || refusal.Status == 403 || refusal.Status == 302 || refusal.Status == 303 {
709 r.terminal("signin")
710 }
711 if refusal.Status == 409 && targeted(s.Type) {
712 r.resetAfterFailure(s)
713 }
714 break
715 }
716 }
717 if !pause(ctx, delay) {
718 if l.ctx.Err() == nil && targeted(s.Type) {
719 r.resetAfterFailure(s)
720 }
721 break
722 }
723 delay = min(delay*2, 5*time.Second)
724 }
725 cancel()
726 // Chat is not echoed by the hub. Report the HTTP outcome, not
727 // merely queue acceptance, so native clients can show sent lines
728 // and make failures visible without claiming delivery to peers.
729 if s.Type == protocol.TypeChat {
730 typ := "send-failed"
731 if accepted {
732 typ = "sent"
733 }
734 r.emit(Event{Type: typ, Signal: &s})
735 }
736 }
737 }
738}
739func (r *Room) iceLoop() {
740 var timer *time.Timer
741 var tick <-chan time.Time
742 defer func() {
743 if timer != nil {
744 timer.Stop()
745 }
746 }()
747 for {
748 select {
749 case <-r.ctx.Done():
750 return
751 case <-r.iceWake:
752 case <-tick:
753 }
754 s := r.Snapshot()
755 if s.Phase != "admitted" {
756 continue
757 }
758 ice, err := r.t.ice(r.ctx, s.Peer)
759 next := time.Minute
760 if err == nil {
761 if r.Snapshot().Peer != s.Peer {
762 continue
763 }
764 r.emit(Event{Type: "ice", ICE: &ice})
765 if ice.RenewAt == 0 {
766 tick = nil
767 if timer != nil {
768 timer.Stop()
769 }
770 continue
771 }
772 next = max(time.Until(time.Unix(ice.RenewAt, 0)), time.Second)
773 } else {
774 r.emit(Event{Type: "ice-unavailable"})
775 }
776 if timer == nil {
777 timer = time.NewTimer(next)
778 } else {
779 timer.Reset(next)
780 }
781 tick = timer.C
782 }
783}
784
785func (r *Room) dropLaneLocked(peer protocol.PeerID) {
786 if l := r.lanes[peer]; l != nil {
787 l.cancel()
788 delete(r.lanes, peer)
789 }
790 delete(r.last, peer)
791 delete(r.seq, peer)
792 delete(r.blocked, peer)
793}
794func (r *Room) resetAfterFailure(s protocol.Signal) {
795 r.mu.Lock()
796 if r.stopped || r.state.Peer != s.Peer || r.generationLocked(s.To) != s.Gen {
797 r.mu.Unlock()
798 return
799 }
800 r.blocked[s.To] = true
801 r.mu.Unlock()
802 // The control lane has the same retry/deadline contract as other sends;
803 // it can overtake the discarded old-generation target queue.
804 _ = r.Send(protocol.TypeReset, string(s.To), 0, "")
805}
806