Files
Piik/internal/server/signal/router.go
T
AAA 083c9273a7 fix(routing): retain connected candidates during delayed viewer feedback
When Viewer progress and first-frame confirmations are delayed, an already connected P2P candidate can be discarded at the five-second scheduling boundary if another candidate is available. This can turn a working initial connection into another connection attempt.

Host and relay parents now report transport progress through the existing room router. The router checks the exact participant/session, revision and connection; only the candidate Viewer can confirm first-frame readiness. The total operation deadline and the routing algorithm are unchanged. Browser and Native senders use their existing prepared-child owners, and discarded preparations cannot report progress.

The loopback benchmark now identifies Peer and SFU connections from room signaling, excluding local codec probes and encoding-pool transports from media-edge counts. Its optional old Viewer-only quality canary remains unsuitable for the current sender-owned quality contract and is recorded in TODO.

Validation:

- `npm run check`: type checks, 1,000 tests and production Web build passed.
- `npm run check:go-core`: formatting, Go tests, vet and supported Windows/Linux builds passed; Darwin capture remains a native-runner check.
- Documentation and project-state checks passed.
- Released v1.5.0 versus this candidate, delaying Viewer transport/ready feedback by six seconds: the old candidate connection closed after the scheduling boundary despite 79 decoded frames; the fixed connection stayed intact. Browser Host, Native Host and relay-parent paths were exercised, with another Viewer continuing to play.
- Three-Viewer loopback topology and relay-departure recovery passed; Host and relay copy limits remained valid.
- Actual App public-link signaling, with isolated 1/8/20-second packet loss on one Browser Viewer: playback recovered in all three cases and the control Viewer continued. Every test process, port and isolated profile was cleaned up.

The original reporter's browser/network and paired diagnostics are unavailable. These bounded tests establish the repaired mechanism, not the cause of every brief connection or a representative NAT success rate.

## Release notes

### 中文

这是一次连接稳定性修复,改善进入房间时的 P2P 连接过程。

- 修复观众的连接确认反馈较慢时,已接通的线路可能被提前拆除,造成重复连接或延迟出画的问题。修复覆盖 App 分享、浏览器分享和观众中继。
- 使用 App 公网邀请或本地模式时,房主更新 App 后重新分享即可。使用已部署站点时,请管理员更新 Server,并刷新页面。

### English

This patch improves P2P connection stability when joining a room.

- Fixed an established connection being discarded prematurely when Viewer confirmations arrive late, causing repeated connection attempts or delayed playback. The fix covers App sharing, browser sharing and Viewer relays.
- For App public invitations or Local mode, the Host should update the App and start sharing again. For a deployed site, update the Server and refresh the page.
2026-09-17 15:18:08 +08:00

1522 lines
52 KiB
Go

package signal
// The router owns no lock. signal.Server.mu (routerOptions.mu) is held by the
// caller of every method except close, hooks are called with it held, and the
// goroutines the router starts (pump driver, media prepares, drains and timer
// callbacks) take it themselves. After unlocked I/O, revalidate the owning
// room, operation and resource before committing effects under the lock.
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"os"
"strings"
"sync"
"time"
"github.com/TNTcraftHIM/Piik/internal/server/ordered"
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
"github.com/TNTcraftHIM/Piik/internal/server/room"
"github.com/TNTcraftHIM/Piik/internal/server/route"
"github.com/TNTcraftHIM/Piik/internal/server/sfu"
)
const (
defaultRouteOperationTimeoutMs int64 = 20_000
)
func routeDebugEnabled() bool {
return routeDebugFlag(os.Getenv("PIIK_DEBUG")) ||
slog.Default().Enabled(context.Background(), slog.LevelDebug)
}
func routeDebugFlag(value string) bool {
for _, section := range strings.Split(value, ",") {
if strings.TrimSpace(section) == "route" {
return true
}
}
return false
}
// routeDebugSink is the injected route-diagnostic sink; nil disables route
// events. The signaling effect layer owns environment and logger policy.
func routeDebugSink() func(string, ...any) {
if !routeDebugEnabled() {
return nil
}
return slog.Info
}
// sfuFallback is SfuFallbackOptions. A zero timeout means the TS default.
type sfuFallback struct {
media sfu.Runtime
admission *sfu.Admission
prepareTimeoutMs int64
}
// routerHooks are the signaling server callbacks of HybridMediaRouterOptions.
// Every hook is called with mu held and must not block. shareGeneration
// returns "" where the TS returned undefined; routesChanged may be nil.
type routerHooks struct {
sendToSession func(sessionID string, message protocol.ServerMessage)
shareGeneration func(roomID string) string
routesChanged func(roomID string)
}
// routerOptions supplies the shared mutex and timer factory.
// now and afterFunc default to the wall clock.
type routerOptions struct {
mu *sync.Mutex
store *room.Store
endpointMediaCopyCapacity int
sfu *sfuFallback
hooks routerHooks
now func() int64
afterFunc func(time.Duration, func()) func() bool
}
// authenticatedRouteParticipant is AuthenticatedRouteParticipant; routePolicy
// is nil where the TS key was absent.
type authenticatedRouteParticipant struct {
roomID string
role protocol.Role
peerID string
sessionID string
routePolicy *protocol.RoutePolicy
}
// hybridAuthenticationState is HybridAuthenticationState.
type hybridAuthenticationState struct {
routeRevision int64
routeAssignment protocol.ParticipantRouteAssignment
}
// activeViewerMediaEdge is ActiveViewerMediaEdge; upstream is peer or sfu.
type activeViewerMediaEdge struct {
revision int64
connectionID string
upstream protocol.MediaRouteUpstream
}
// signalAuthorization is the `boolean | "probe" | undefined` result of
// peerSignalAuthorization. signalAuthorizationUnassigned is the TS undefined:
// the signaling server falls back to the committed-edge assignment.
type signalAuthorization int
const (
signalAuthorizationUnassigned signalAuthorization = iota
signalAuthorizationDenied
signalAuthorizationAllowed
signalAuthorizationProbe
)
// peerSignalInput is the peerSignalAuthorization input; descriptionType is
// "" where the TS key was absent.
type peerSignalInput struct {
roomID string
sourcePeerID string
sourceSessionID string
targetPeerID string
targetSessionID string
connectionID string
signalKind string
descriptionType string
}
// peerSignalDebugInput is the debugPeerSignal input; candidateOrigin and
// descriptionType are "" where the TS keys were absent.
type peerSignalDebugInput struct {
roomID string
sourcePeerID string
targetPeerID string
signalKind string
candidateOrigin protocol.CandidateSignalOrigin
descriptionType string
authorization signalAuthorization
}
// roomRuntime is RoomRuntime. pumping is the `pump` promise used as a
// single-flight token; deadlineGeneration is bumped by every clearDeadline so
// a callback that lost the Stop race sees it is stale.
type roomRuntime struct {
hostPeerID string
controller *route.Controller
// Capacities survive stopRoom in insertion order.
advertisedCapacityByViewer ordered.Map[string, int]
requested bool
pumping bool
deadlineStop func() bool
deadlineGeneration uint64
}
// router is HybridMediaRouter.
type router struct {
mu *sync.Mutex
store *room.Store
capacity int
sfu *sfuFallback
hooks routerHooks
now func() int64
afterFunc func(time.Duration, func()) func() bool
// Rooms, drain tasks and waiters are walked in insertion
// order.
rooms ordered.Map[string, *roomRuntime]
resourceWaiters ordered.Map[string, struct{}]
sfuDrainTasks ordered.Map[sfu.SubscriptionFence, *sfuDrainTask]
// inflight counts the goroutines that will re-acquire mu: the pump
// drivers, fresh SFU config issues and drains. It is the TS microtask
// backlog; tests wait for it to reach zero where the TS awaited.
inflight int
closing bool
}
// newRouter is the HybridMediaRouter constructor.
func newRouter(options routerOptions) *router {
if err := protocol.AssertEndpointMediaCopyCapacity(options.endpointMediaCopyCapacity); err != nil {
panic(err)
}
r := &router{
mu: options.mu,
store: options.store,
capacity: options.endpointMediaCopyCapacity,
sfu: options.sfu,
hooks: options.hooks,
now: options.now,
afterFunc: options.afterFunc,
}
if r.now == nil {
r.now = func() int64 { return time.Now().UnixMilli() }
}
if r.afterFunc == nil {
r.afterFunc = func(d time.Duration, fn func()) func() bool {
return time.AfterFunc(d, fn).Stop
}
}
return r
}
// io is one TS `await` on external work: mu is released around fn and always
// re-acquired, including while a panic unwinds. That last part is the reason
// this is a helper rather than an inline Unlock/Lock pair: recoverPump turns a
// panic raised inside such a window into the TS pump rejection, and its
// caller's `defer r.mu.Unlock()` would otherwise unlock a mutex nobody holds
// (an unrecoverable runtime error). Nothing may touch router state inside fn.
func (r *router) io(fn func()) {
r.mu.Unlock()
defer r.mu.Lock()
fn()
}
// close retires room operations and waits for their exact physical resources.
func (r *router) close(ctx context.Context) error {
r.closing = true
for _, roomID := range r.rooms.Keys() {
r.clearRoom(roomID)
}
r.resourceWaiters.Clear()
fallback := r.sfu
if fallback == nil {
return nil
}
for _, fence := range fallback.admission.BeginDrainAll() {
r.scheduleSfuPublicationDrain(fence)
}
type entry struct {
key sfu.SubscriptionFence
task *sfuDrainTask
}
var snapshot []entry
for key, task := range r.sfuDrainTasks.All() {
snapshot = append(snapshot, entry{key, task})
}
var errs []error
for _, current := range snapshot {
key, task := current.key, current.task
if task.operation != nil {
if err := r.awaitDrain(ctx, task.operation); err != nil {
errs = append(errs, err)
continue
}
}
if registered, _ := r.sfuDrainTasks.Get(key); registered == task {
if err := r.awaitDrain(ctx, r.runSfuDrain(key, task)); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return fmt.Errorf("SFU drain failed during shutdown: %w", errors.Join(errs...))
}
return nil
}
func (r *router) connectParticipant(input authenticatedRouteParticipant) hybridAuthenticationState {
rm := r.room(input.roomID)
if input.role == protocol.RoleHost {
if rm.controller != nil && rm.hostPeerID != input.peerID {
r.clearDeadline(rm)
// rm.hostPeerID always equals the controller's, so this branch
// takes RebindHostIdentity's clearing path: every committed edge
// goes, which is what the TS per-viewer deleteConnectionId loop
// then restated into the mirror.
r.releaseResources(rm.controller.RebindHostIdentity(input.peerID, input.sessionID, r.nowPtr()))
}
rm.hostPeerID = input.peerID
if rm.controller == nil {
r.createController(input.roomID, rm, input)
}
} else {
capacity, _ := rm.advertisedCapacityByViewer.Get(input.peerID)
rm.advertisedCapacityByViewer.Set(input.peerID, capacity)
}
if rm.controller != nil {
capacity := r.capacity
if input.role != protocol.RoleHost {
capacity, _ = rm.advertisedCapacityByViewer.Get(input.peerID)
}
r.releaseResources(rm.controller.UpsertParticipant(route.ParticipantInput{
PeerID: input.peerID,
Role: input.role,
SessionID: input.sessionID,
EffectiveDownstreamCapacity: capacity,
}, r.nowPtr()))
}
state := hybridAuthenticationState{routeAssignment: emptyAssignment()}
if rm.controller != nil {
current := rm.controller.Snapshot()
state.routeRevision = current.Revision
state.routeAssignment = assignmentFor(r.assignments(input.roomID, current, nil, "", ""), input.peerID)
}
return state
}
func (r *router) completeAuthentication(participant authenticatedRouteParticipant, _ hybridAuthenticationState) {
rm, _ := r.rooms.Get(participant.roomID)
if rm == nil || rm.controller == nil {
return
}
r.broadcastActive(participant.roomID, rm)
r.sendFreshSfuConfig(participant)
r.requestPump(participant.roomID)
}
func (r *router) setPaused(roomID string, paused bool) {
rm, _ := r.rooms.Get(roomID)
if rm == nil || rm.controller == nil {
return
}
before := rm.controller.Revision()
r.releaseResources(rm.controller.SetPaused(paused, r.nowPtr()))
if rm.controller.Revision() != before {
r.broadcastActive(roomID, rm)
}
if !paused {
r.requestPump(roomID)
}
}
func (r *router) routeDiagnosticSnapshot(roomID string) protocol.RouteDiagnosticSnapshot {
if rm, _ := r.rooms.Get(roomID); rm != nil && rm.controller != nil {
return rm.controller.RouteDiagnosticSnapshot(r.now())
}
return protocol.RouteDiagnosticSnapshot{Children: []protocol.RouteDiagnosticChild{}, Operation: nil}
}
func (r *router) observeQualityEvidence(roomID string, evidence route.RouteQualityEvidenceInput) route.RouteQualityEvidenceResult {
result := route.QualityEvidenceRejected
if rm, _ := r.rooms.Get(roomID); rm != nil && rm.controller != nil {
result = rm.controller.ObserveQualityEvidence(evidence)
}
if result != route.QualityEvidenceRejected && routeDebugEnabled() {
upstream := "sfu"
if evidence.Upstream.Kind == route.UpstreamPeer {
upstream = "p2p:" + r.debugPeer(roomID, evidence.Upstream.PeerID)
}
metrics := evidence.Metrics
var packetLossPercent any
if percent, ok := protocol.PacketLossPercentFromDeltas(
intFloat(metrics.PacketsReceivedDelta), intFloat(metrics.PacketsLostDelta)); ok {
packetLossPercent = percent
}
r.debug(roomID, "viewer-quality-evidence",
"viewer", r.debugPeer(roomID, evidence.ChildPeerID),
"upstream", upstream,
"natTraversalPath", metrics.NatTraversalPath,
"framesPerSecond", optional(metrics.FramesPerSecond),
"bitrateKbps", optional(metrics.BitrateKbps),
"width", optional(metrics.Width),
"height", optional(metrics.Height),
"framesDecoded", optional(metrics.FramesDecodedDelta),
"freezes", optional(metrics.FreezeCountDelta),
"pauses", optional(metrics.PauseCountDelta),
"packetsReceivedDelta", optional(metrics.PacketsReceivedDelta),
"packetsLostDelta", optional(metrics.PacketsLostDelta),
"packetLossPercent", packetLossPercent,
"rttMs", optional(metrics.RttMs),
"jitterMs", optional(metrics.JitterMs))
}
return result
}
func (r *router) observeSenderQualityEvidence(participant authenticatedRouteParticipant, message protocol.SenderQualityEvidenceMessage) bool {
rm, _ := r.rooms.Get(participant.roomID)
if rm == nil || rm.controller == nil {
return false
}
controller := rm.controller
snapshot := controller.Snapshot()
before := snapshot.Revision
endpointCapacity := r.capacity
if participant.role != protocol.RoleHost {
advertised, _ := rm.advertisedCapacityByViewer.Get(participant.peerID)
endpointCapacity = min(advertised, r.capacity)
}
copyContext := r.qualityCopyContext(snapshot, rm.hostPeerID, participant.peerID, endpointCapacity, qualitySample{
kind: route.UpstreamPeer,
childPeerID: message.ChildPeerID,
routeRevision: int64(message.RouteRevision),
connectionID: message.ConnectionID,
})
var senderIdentity *string
if message.RtpStatsID != nil && *message.RtpStatsID != "" &&
message.TrackIdentifier != nil && *message.TrackIdentifier != "" {
identity := *message.RtpStatsID + "\x00" + *message.TrackIdentifier
senderIdentity = &identity
}
input := route.SenderQualityEvidenceInput{
ParentPeerID: participant.peerID,
ParentSessionID: participant.sessionID,
ChildPeerID: message.ChildPeerID,
RouteRevision: int64(message.RouteRevision),
ConnectionID: message.ConnectionID,
SenderIdentity: senderIdentity,
SampleTimestampMs: sampleTimestamp(message.SampleTimestampMs),
State: route.SenderQualityState(message.State),
AcceptedAtMs: r.now(),
}
result := controller.ObserveSenderQualityEvidence(input, r.commitReservation)
if result.Accepted && routeDebugEnabled() {
diagnostics := message.Diagnostics
details := []any{
"parent", r.debugPeer(participant.roomID, participant.peerID),
"child", r.debugPeer(participant.roomID, message.ChildPeerID),
"routeRevision", int64(message.RouteRevision),
}
details = append(details, copyContext.kv()...)
details = append(details,
"state", message.State,
"natTraversalPath", diagnostics.NatTraversalPath,
"reason", optional(diagnostics.Reason),
"framesPerSecond", optional(diagnostics.FramesPerSecond),
"bitrateKbps", optional(diagnostics.BitrateKbps),
"captureFramesPerSecond", nullable(diagnostics.CaptureFramesPerSecond),
"mediaSourceFramesPerSecond", nullable(diagnostics.MediaSourceFramesPerSecond),
"width", nullable(diagnostics.Width),
"height", nullable(diagnostics.Height),
"availableOutgoingKbps", nullable(diagnostics.AvailableOutgoingKbps),
"rttMs", nullable(diagnostics.RttMs),
"packetLossPercent", nullable(diagnostics.PacketLossPercent))
r.debug(participant.roomID, "sender-quality-evidence", details...)
}
r.releaseResources(result.Released)
if result.Committed {
r.resourceWaiters.Delete(participant.roomID)
}
if controller.Revision() != before {
r.broadcastActive(participant.roomID, rm)
}
r.sendRouteFailures(participant.roomID, result.FailedPeerIDs, controller.Revision())
if result.Accepted {
r.requestPump(participant.roomID)
}
return result.Accepted
}
func (r *router) observeSfuPublisherQualityEvidence(participant authenticatedRouteParticipant, message protocol.SfuPublisherQualityEvidenceMessage) bool {
rm, _ := r.rooms.Get(participant.roomID)
if rm == nil || rm.controller == nil || participant.role != protocol.RoleHost {
return false
}
controller := rm.controller
snapshot := controller.Snapshot()
operation := snapshot.Operation
candidateChildPeerID, candidateConnectionID := "", ""
if operation != nil {
candidateChildPeerID = operation.ChildPeerID
if operation.Current != nil {
candidateConnectionID = operation.Current.ConnectionID
}
}
before := snapshot.Revision
copyContext := r.qualityCopyContext(snapshot, participant.peerID, participant.peerID, r.capacity, qualitySample{
kind: route.UpstreamSfu,
routeRevision: int64(message.RouteRevision),
publicationGeneration: message.PublicationGeneration,
hostSessionID: participant.sessionID,
})
input := route.SfuPublisherQualityEvidenceInput{
HostPeerID: participant.peerID,
HostSessionID: participant.sessionID,
PublicationGeneration: message.PublicationGeneration,
RouteRevision: int64(message.RouteRevision),
State: route.SenderQualityState(message.State),
SampleTimestampMs: sampleTimestamp(message.SampleTimestampMs),
AcceptedAtMs: r.now(),
}
result := controller.ObserveSfuPublisherQualityEvidence(input, r.commitReservation)
if result.Accepted && routeDebugEnabled() {
diagnostics := message.Diagnostics
details := []any{"routeRevision", int64(message.RouteRevision)}
details = append(details, copyContext.kv()...)
details = append(details,
"state", message.State,
"reason", optional(diagnostics.Reason),
"framesPerSecond", optional(diagnostics.FramesPerSecond),
"bitrateKbps", optional(diagnostics.BitrateKbps),
"captureFramesPerSecond", nullable(diagnostics.CaptureFramesPerSecond),
"mediaSourceFramesPerSecond", nullable(diagnostics.MediaSourceFramesPerSecond),
"width", nullable(diagnostics.Width),
"height", nullable(diagnostics.Height),
"videoEncodingCount", nullable(diagnostics.VideoEncodingCount),
"activeVideoEncodingCount", nullable(diagnostics.ActiveVideoEncodingCount),
"availableOutgoingKbps", nullable(diagnostics.AvailableOutgoingKbps),
"rttMs", nullable(diagnostics.RttMs),
"packetLossPercent", nullable(diagnostics.PacketLossPercent))
r.debug(participant.roomID, "sfu-publisher-quality-evidence", details...)
}
r.releaseResources(result.Released)
if result.Committed && candidateChildPeerID != "" && candidateConnectionID != "" {
r.resourceWaiters.Delete(participant.roomID)
}
if controller.Revision() != before {
r.broadcastActive(participant.roomID, rm)
}
r.sendRouteFailures(participant.roomID, result.FailedPeerIDs, controller.Revision())
if result.Accepted {
r.requestPump(participant.roomID)
}
return result.Accepted
}
func (r *router) resetSenderQuality(participant authenticatedRouteParticipant) {
rm, _ := r.rooms.Get(participant.roomID)
if rm == nil || rm.controller == nil {
return
}
before := rm.controller.Revision()
r.releaseResources(rm.controller.ResetSenderQuality(participant.peerID, participant.sessionID, r.now()))
if rm.controller.Revision() != before {
r.broadcastActive(participant.roomID, rm)
}
r.requestPump(participant.roomID)
}
func (r *router) isActivePeerParentOf(roomID, parentPeerID, childPeerID string) bool {
_, activeParent, ok := r.resolveActivePeerEdge(roomID, childPeerID)
return ok && activeParent == parentPeerID
}
// resolveActivePeerEdge returns false when the child has no active Peer edge.
func (r *router) resolveActivePeerEdge(roomID, childPeerID string) (revision int64, parentPeerID string, ok bool) {
edge, found := r.resolveActiveViewerMediaEdge(roomID, childPeerID)
if !found || edge.upstream.Kind != "peer" {
return 0, "", false
}
return edge.revision, edge.upstream.PeerID, true
}
func (r *router) resolveActiveViewerMediaEdge(roomID, childPeerID string) (activeViewerMediaEdge, bool) {
rm, _ := r.rooms.Get(roomID)
if rm == nil || rm.controller == nil {
return activeViewerMediaEdge{}, false
}
snapshot := rm.controller.Snapshot()
edge, hasEdge := snapshot.UpstreamByViewer.Get(childPeerID)
if !hasEdge || !r.pathIsPhysical(roomID, snapshot, childPeerID) {
return activeViewerMediaEdge{}, false
}
upstream := protocol.SfuUpstream()
if edge.Kind == route.UpstreamPeer {
upstream = protocol.PeerUpstream(edge.ParentPeerID)
}
return activeViewerMediaEdge{
revision: snapshot.Revision,
connectionID: edge.ConnectionID,
upstream: upstream,
}, true
}
func (r *router) getViewerRouteUpstream(roomID, viewerPeerID string) protocol.MediaRouteUpstream {
rm, _ := r.rooms.Get(roomID)
if rm == nil || rm.controller == nil {
return protocol.NoUpstream()
}
return assignmentFor(r.assignments(roomID, rm.controller.Snapshot(), nil, "", ""), viewerPeerID).Upstream
}
// A retired server candidate must never be mistaken for a parent-initiated
// replacement. Its ID remains opaque to clients; no retired-ID history is needed.
const candidateConnectionIDPrefix = "route_"
// peerSignalAuthorization resolves candidate and retained media by connection,
// since both may belong to the same parent/child pair during overlap.
func (r *router) peerSignalAuthorization(input peerSignalInput) signalAuthorization {
rm, _ := r.rooms.Get(input.roomID)
if rm == nil || rm.controller == nil {
return signalAuthorizationUnassigned
}
controller := rm.controller
snapshot := controller.Snapshot()
operation := snapshot.Operation
if operation != nil && operation.Current != nil && operation.Current.Tuple.Kind == route.UpstreamPeer {
current := operation.Current
parent, hasParent := r.connectedPeer(input.roomID, current.Tuple.ParentPeerID)
child, hasChild := r.store.GetConnectedViewer(input.roomID, operation.ChildPeerID)
parentToChild := input.sourcePeerID == current.Tuple.ParentPeerID &&
input.targetPeerID == operation.ChildPeerID
childToParent := input.sourcePeerID == operation.ChildPeerID &&
input.targetPeerID == current.Tuple.ParentPeerID
if (parentToChild || childToParent) && input.connectionID == current.ConnectionID {
parentSession, childSession := input.targetSessionID, input.sourceSessionID
if parentToChild {
parentSession, childSession = input.sourceSessionID, input.targetSessionID
}
if hasParent && hasChild &&
parent.SessionID == parentSession &&
child.SessionID == childSession &&
(input.signalKind == "candidate" ||
(parentToChild && input.descriptionType == "offer") ||
(childToParent && input.descriptionType == "answer")) {
return signalAuthorizationProbe
}
return signalAuthorizationDenied
}
}
childPeerID := childForPair(snapshot, input.sourcePeerID, input.targetPeerID)
if childPeerID == "" {
return signalAuthorizationUnassigned
}
edge, hasEdge := snapshot.UpstreamByViewer.Get(childPeerID)
if !hasEdge || edge.Kind != route.UpstreamPeer || !edge.PhysicalActive {
return signalAuthorizationUnassigned
}
parentToChild := input.sourcePeerID == edge.ParentPeerID
sessionsMatch := (parentToChild &&
input.sourceSessionID == edge.ParentSessionID &&
input.targetSessionID == edge.ChildSessionID) ||
(!parentToChild &&
input.sourceSessionID == edge.ChildSessionID &&
input.targetSessionID == edge.ParentSessionID)
if !sessionsMatch {
return signalAuthorizationDenied
}
if edge.Transport == route.TransportDirect &&
parentToChild &&
input.signalKind == "description" &&
input.descriptionType == "offer" &&
input.connectionID != edge.ConnectionID {
if strings.HasPrefix(input.connectionID, candidateConnectionIDPrefix) {
return signalAuthorizationDenied
}
adopted := controller.AdoptDirectConnection(route.AdoptDirectConnectionInput{
EdgeGuard: route.EdgeGuard{
ChildPeerID: childPeerID,
ChildSessionID: edge.ChildSessionID,
ParentSessionID: edge.ParentSessionID,
RouteRevision: snapshot.Revision,
ConnectionID: edge.ConnectionID,
},
NewConnectionID: input.connectionID,
}, r.now())
if adopted.Accepted {
r.releaseResources(adopted.Released)
if adopted.ActiveRevision != snapshot.Revision {
r.broadcastActive(input.roomID, rm)
r.requestPump(input.roomID)
}
return signalAuthorizationAllowed
}
return signalAuthorizationDenied
}
if input.connectionID == edge.ConnectionID {
return signalAuthorizationAllowed
}
return signalAuthorizationDenied
}
func (r *router) debugPeerSignal(input peerSignalDebugInput) {
if !routeDebugEnabled() {
return
}
var authorization any
switch input.authorization {
case signalAuthorizationUnassigned:
authorization = "assignment"
case signalAuthorizationDenied:
authorization = false
case signalAuthorizationAllowed:
authorization = true
case signalAuthorizationProbe:
authorization = "probe"
}
r.debug(input.roomID, "peer-signal",
"source", r.debugPeer(input.roomID, input.sourcePeerID),
"target", r.debugPeer(input.roomID, input.targetPeerID),
"signalKind", input.signalKind,
"candidateOrigin", nullString(string(input.candidateOrigin)),
"descriptionType", nullString(input.descriptionType),
"authorization", authorization)
}
func (r *router) setViewerRelayCapacity(participant authenticatedRouteParticipant, downstreamEdges int) {
rm := r.room(participant.roomID)
r.debug(participant.roomID, "relay-capacity-received",
"participant", r.debugPeer(participant.roomID, participant.peerID),
"downstreamEdges", downstreamEdges)
rm.advertisedCapacityByViewer.Set(participant.peerID, downstreamEdges)
if rm.controller != nil &&
rm.controller.SetEffectiveCapacity(participant.peerID, participant.sessionID, downstreamEdges, r.nowPtr()) {
r.requestPump(participant.roomID)
}
}
func (r *router) handleRouteReady(participant authenticatedRouteParticipant, message protocol.RouteReadyMessage) {
if participant.role != protocol.RoleViewer || message.Phase != "prepare" {
return
}
revision := int64(message.Revision)
r.debug(participant.roomID, "route-ready-received",
"participant", r.debugPeer(participant.roomID, participant.peerID),
"revision", revision)
rm, _ := r.rooms.Get(participant.roomID)
if rm == nil || rm.controller == nil {
return
}
controller := rm.controller
operation := controller.Operation()
if operation == nil || operation.Current == nil ||
operation.ChildPeerID != participant.peerID ||
operation.ChildSessionID != participant.sessionID ||
operation.Current.Revision != revision {
return
}
current := operation.Current
before := controller.Revision()
settled := controller.CandidateReady(route.CandidateGuard{
ChildPeerID: participant.peerID,
ChildSessionID: participant.sessionID,
Revision: revision,
ConnectionID: current.ConnectionID,
}, r.now(), r.commitReservation, route.CandidateProof{RelativeQualityApproved: message.QualityApproved})
r.debug(participant.roomID, "route-ready-settled",
"participant", r.debugPeer(participant.roomID, participant.peerID),
"revision", revision,
"accepted", settled.Accepted,
"exhausted", len(settled.FailedPeerIDs) > 0)
r.releaseResources(settled.Released)
if settled.Committed && settled.Accepted {
r.resourceWaiters.Delete(participant.roomID)
}
if controller.Revision() != before {
r.broadcastActive(participant.roomID, rm)
}
r.sendRouteFailures(participant.roomID, settled.FailedPeerIDs, controller.Revision())
r.requestPump(participant.roomID)
}
// Either endpoint may report transport progress or local failure. First-frame
// readiness remains owned by the candidate Viewer in handleRouteReady.
func (r *router) ownsCandidateEndpoint(participant authenticatedRouteParticipant, operation *route.OperationSnapshot) bool {
if participant.role == protocol.RoleViewer && participant.peerID == operation.ChildPeerID &&
participant.sessionID == operation.ChildSessionID {
return true
}
if operation.Current.Tuple.Kind == route.UpstreamPeer && participant.peerID == operation.Current.Tuple.ParentPeerID {
peer, ok := r.connectedPeer(participant.roomID, participant.peerID)
return ok && peer.SessionID == participant.sessionID
}
return false
}
func (r *router) handleRouteTransportConnected(participant authenticatedRouteParticipant, message protocol.RouteTransportConnectedMessage) {
rm, _ := r.rooms.Get(participant.roomID)
if rm == nil || rm.controller == nil {
return
}
controller := rm.controller
operation := controller.Operation()
revision := int64(message.Revision)
if operation == nil || operation.Current == nil ||
operation.Current.Revision != revision ||
operation.Current.ConnectionID != message.ConnectionID ||
operation.Current.Tuple.Kind != route.UpstreamPeer ||
!r.ownsCandidateEndpoint(participant, operation) {
return
}
before := controller.Revision()
progressed := controller.CandidateTransportConnected(route.CandidateGuard{
ChildPeerID: operation.ChildPeerID,
ChildSessionID: operation.ChildSessionID,
Revision: revision,
ConnectionID: message.ConnectionID,
}, r.now())
r.releaseResources(progressed.Released)
if controller.Revision() != before {
r.broadcastActive(participant.roomID, rm)
}
r.sendRouteFailures(participant.roomID, progressed.FailedPeerIDs, controller.Revision())
after := controller.Operation()
if !progressed.Accepted || after == nil || after.WakeAtMs != operation.WakeAtMs {
r.requestPump(participant.roomID)
}
}
func (r *router) handleRouteMediaUnavailable(participant authenticatedRouteParticipant, message protocol.RouteMediaUnavailableMessage) {
if participant.role != protocol.RoleViewer {
return
}
revision := int64(message.Revision)
r.debug(participant.roomID, "sfu-media-unavailable",
"participant", r.debugPeer(participant.roomID, participant.peerID),
"revision", revision)
rm, _ := r.rooms.Get(participant.roomID)
if rm == nil || rm.controller == nil {
return
}
snapshot := rm.controller.Snapshot()
edge, hasEdge := snapshot.UpstreamByViewer.Get(participant.peerID)
if snapshot.Paused || snapshot.Revision != revision ||
!hasEdge || edge.Kind != route.UpstreamSfu ||
edge.ChildSessionID != participant.sessionID {
return
}
rm.controller.InvalidateEdge(route.EdgeGuard{
ChildPeerID: participant.peerID,
ChildSessionID: participant.sessionID,
RouteRevision: snapshot.Revision,
ConnectionID: edge.ConnectionID,
}, r.nowPtr())
r.requestPump(participant.roomID)
}
func (r *router) handleRouteFailed(participant authenticatedRouteParticipant, message protocol.RouteFailedMessage) {
connectionID := ""
if message.ConnectionID != nil {
connectionID = *message.ConnectionID
}
revision := int64(message.Revision)
r.debug(participant.roomID, "route-failed-received",
"participant", r.debugPeer(participant.roomID, participant.peerID),
"role", participant.role,
"phase", message.Phase,
"revision", revision,
"hasConnectionId", connectionID != "")
rm, _ := r.rooms.Get(participant.roomID)
if rm == nil || rm.controller == nil {
return
}
controller := rm.controller
snapshot := controller.Snapshot()
if message.Phase == "prepare" {
operation := snapshot.Operation
if operation == nil || operation.Current == nil || operation.Current.Revision != revision {
return
}
current := operation.Current
ownsEndpoint := r.ownsCandidateEndpoint(participant, operation)
ownsPublication := false
if current.Tuple.Kind == route.UpstreamSfu && participant.role == protocol.RoleHost &&
participant.peerID == rm.hostPeerID {
host, ok := r.store.GetConnectedHost(participant.roomID)
ownsPublication = ok && host.SessionID == participant.sessionID
}
if !ownsEndpoint && !ownsPublication {
return
}
if connectionID != "" && connectionID != current.ConnectionID {
return
}
before := snapshot.Revision
settled := controller.CandidateFailed(route.CandidateGuard{
ChildPeerID: operation.ChildPeerID,
ChildSessionID: operation.ChildSessionID,
Revision: current.Revision,
ConnectionID: current.ConnectionID,
}, r.now())
r.releaseResources(settled.Released)
if controller.Revision() != before {
r.broadcastActive(participant.roomID, rm)
}
r.sendRouteFailures(participant.roomID, settled.FailedPeerIDs, controller.Revision())
r.requestPump(participant.roomID)
return
}
if snapshot.Revision != revision {
return
}
if participant.role == protocol.RoleHost {
if connectionID != "" && controller.InvalidateDirectEdgeFromParent(route.ParentEdgeGuard{
ParentPeerID: participant.peerID,
ParentSessionID: participant.sessionID,
RouteRevision: snapshot.Revision,
ConnectionID: connectionID,
}, r.nowPtr()) {
r.requestPump(participant.roomID)
return
}
publication := snapshot.HostPublication
if publication == nil || publication.HostSessionID != participant.sessionID ||
(connectionID != "" && connectionID != publication.ConnectionID) {
return
}
controller.InvalidateHostPublication(route.PublicationGuard{
HostSessionID: participant.sessionID,
RouteRevision: snapshot.Revision,
Generation: publication.Generation,
ConnectionID: publication.ConnectionID,
}, r.nowPtr())
r.releaseResources(controller.RetireHostPublication(route.PublicationGuard{
HostSessionID: participant.sessionID,
RouteRevision: controller.Revision(),
Generation: publication.Generation,
ConnectionID: publication.ConnectionID,
}))
r.broadcastActive(participant.roomID, rm)
r.requestPump(participant.roomID)
return
}
edge, hasEdge := snapshot.UpstreamByViewer.Get(participant.peerID)
if !hasEdge || edge.ChildSessionID != participant.sessionID ||
(connectionID != "" && connectionID != edge.ConnectionID) {
return
}
guard := route.EdgeGuard{
ChildPeerID: participant.peerID,
ChildSessionID: participant.sessionID,
RouteRevision: snapshot.Revision,
ConnectionID: edge.ConnectionID,
}
if edge.Kind == route.UpstreamPeer {
guard.ParentSessionID = edge.ParentSessionID
}
invalidated := controller.InvalidateEdge(guard, r.nowPtr())
if invalidated && edge.Usable && snapshot.Paused && r.hooks.routesChanged != nil {
r.hooks.routesChanged(participant.roomID)
}
r.requestPump(participant.roomID)
}
func (r *router) refreshSfu(participant authenticatedRouteParticipant, revision int64) {
rm, _ := r.rooms.Get(participant.roomID)
if rm != nil && rm.controller != nil && revision <= rm.controller.Revision() {
r.sendFreshSfuConfig(participant)
}
}
func (r *router) disconnectParticipant(roomID, peerID, sessionID string) {
rm, _ := r.rooms.Get(roomID)
if rm == nil || rm.controller == nil || !rm.controller.DisconnectSession(peerID, sessionID) {
return
}
r.requestPump(roomID)
}
func (r *router) removeViewer(roomID, peerID string) {
rm, _ := r.rooms.Get(roomID)
if rm != nil {
rm.advertisedCapacityByViewer.Delete(peerID)
}
if rm != nil && rm.controller != nil && rm.controller.ConfirmDeparture(peerID, r.nowPtr()) {
r.requestPump(roomID)
} else if rm != nil && rm.controller == nil && rm.advertisedCapacityByViewer.Len() == 0 {
r.rooms.Delete(roomID)
}
}
// stopRoom preserves advertised capacities across shares.
func (r *router) stopRoom(roomID string) {
type capacity struct {
peerID string
value int
}
var capacities []capacity
if rm, _ := r.rooms.Get(roomID); rm != nil {
for peerID, value := range rm.advertisedCapacityByViewer.All() {
capacities = append(capacities, capacity{peerID, value})
}
}
r.clearRoom(roomID)
if len(capacities) > 0 {
rm := r.room(roomID)
for _, current := range capacities {
rm.advertisedCapacityByViewer.Set(current.peerID, current.value)
}
}
}
func (r *router) deleteRoom(roomID string) {
r.clearRoom(roomID)
}
func (r *router) room(roomID string) *roomRuntime {
rm, ok := r.rooms.Get(roomID)
if !ok {
rm = &roomRuntime{}
r.rooms.Set(roomID, rm)
}
return rm
}
// createController connects viewers in store order.
func (r *router) createController(roomID string, rm *roomRuntime, host authenticatedRouteParticipant) {
routePolicy := host.routePolicy
operationTimeoutMs := defaultRouteOperationTimeoutMs
if r.sfu != nil && r.sfu.prepareTimeoutMs != 0 {
operationTimeoutMs = r.sfu.prepareTimeoutMs
}
rm.controller = route.New(route.Options{
HostPeerID: host.peerID,
DebugRoomID: roomID,
DebugLog: routeDebugSink(),
EndpointMediaCopyCapacity: r.capacity,
OperationTimeoutMs: operationTimeoutMs,
SfuEnabled: r.sfu != nil && !(routePolicy != nil && routePolicy.PeerOnly),
QualityConvergenceEnabled: routePolicy != nil && routePolicy.TopologyOptimization,
NatPredictionEnabled: routePolicy != nil && routePolicy.NatPrediction,
})
rm.controller.UpsertParticipant(route.ParticipantInput{
PeerID: host.peerID,
Role: protocol.RoleHost,
SessionID: host.sessionID,
EffectiveDownstreamCapacity: r.capacity,
}, r.nowPtr())
for _, viewer := range r.store.GetConnectedViewers(roomID) {
capacity, _ := rm.advertisedCapacityByViewer.Get(viewer.PeerID)
rm.controller.UpsertParticipant(route.ParticipantInput{
PeerID: viewer.PeerID,
Role: protocol.RoleViewer,
SessionID: viewer.SessionID,
EffectiveDownstreamCapacity: capacity,
}, r.nowPtr())
}
}
// clearRoom drains publications in admission order.
func (r *router) clearRoom(roomID string) {
rm, ok := r.rooms.Get(roomID)
if !ok {
return
}
r.clearDeadline(rm)
controller := rm.controller
rm.controller = nil
rm.requested = false
if controller != nil {
r.releaseResources(controller.Dispose())
}
r.rooms.Delete(roomID)
r.resourceWaiters.Delete(roomID)
if r.sfu != nil {
for _, fence := range r.sfu.admission.BeginDrainRoom(roomID) {
r.scheduleSfuPublicationDrain(fence)
}
}
}
// assignmentEdge is the value of the `edges` map of assignments().
type assignmentEdge struct {
kind route.UpstreamKind
parentPeerID string
publicationGeneration string
}
// assignments overlays the current attempt when candidate is non-nil
// and otherwise uses only the committed graph. Peers are the host first, then
// the store's viewer order; edges keep the snapshot's insertion
// order, which is the childPeerIds order on the wire.
func (r *router) assignments(
roomID string,
snapshot route.RouteSnapshot,
candidate *route.CandidateTuple,
candidateChildPeerID string,
candidatePublicationGeneration string,
) map[string]protocol.ParticipantRouteAssignment {
assignments := map[string]protocol.ParticipantRouteAssignment{}
rm, _ := r.rooms.Get(roomID)
if rm == nil || rm.hostPeerID == "" {
return assignments
}
hostPeerID := rm.hostPeerID
peerIDs := []string{hostPeerID}
seen := map[string]struct{}{hostPeerID: {}}
for _, peerID := range r.store.GetViewerPeerIDs(roomID) {
if _, duplicate := seen[peerID]; !duplicate {
seen[peerID] = struct{}{}
peerIDs = append(peerIDs, peerID)
}
}
edges := &ordered.Map[string, assignmentEdge]{}
for childPeerID, edge := range snapshot.UpstreamByViewer.All() {
if !edge.PhysicalActive {
continue
}
if edge.Kind == route.UpstreamPeer {
edges.Set(childPeerID, assignmentEdge{kind: route.UpstreamPeer, parentPeerID: edge.ParentPeerID})
} else {
edges.Set(childPeerID, assignmentEdge{kind: route.UpstreamSfu, publicationGeneration: edge.PublicationGeneration})
}
}
publicationGeneration := ""
if snapshot.HostPublication != nil && snapshot.HostPublication.PhysicalActive {
publicationGeneration = snapshot.HostPublication.Generation
}
if candidate != nil && candidateChildPeerID != "" {
// TS delete-then-set: the candidate child moves to the back.
edges.Delete(candidateChildPeerID)
if candidate.Kind == route.UpstreamPeer {
edges.Set(candidateChildPeerID, assignmentEdge{kind: route.UpstreamPeer, parentPeerID: candidate.ParentPeerID})
if old, hasOld := snapshot.UpstreamByViewer.Get(candidateChildPeerID); hasOld && old.Kind == route.UpstreamSfu {
anySfu := false
for _, edge := range edges.Values() {
if edge.kind == route.UpstreamSfu {
anySfu = true
break
}
}
if !anySfu {
publicationGeneration = ""
}
}
} else {
if candidate.Publication != route.PublicationReuse {
for peerID, edge := range edges.All() {
if edge.kind == route.UpstreamSfu {
edges.Delete(peerID)
}
}
publicationGeneration = candidatePublicationGeneration
}
if publicationGeneration != "" {
edges.Set(candidateChildPeerID, assignmentEdge{kind: route.UpstreamSfu, publicationGeneration: publicationGeneration})
}
}
}
var sourceReachable func(peerID string, visited map[string]struct{}) bool
sourceReachable = func(peerID string, visited map[string]struct{}) bool {
if peerID == hostPeerID {
return true
}
if _, again := visited[peerID]; again {
return false
}
visited[peerID] = struct{}{}
edge, ok := edges.Get(peerID)
if !ok {
return false
}
if edge.kind == route.UpstreamSfu {
return publicationGeneration == edge.publicationGeneration
}
return sourceReachable(edge.parentPeerID, visited)
}
for _, peerID := range peerIDs {
edge, hasEdge := edges.Get(peerID)
reachable := sourceReachable(peerID, map[string]struct{}{})
childPeerIDs := []string{}
for childPeerID, child := range edges.All() {
if child.kind == route.UpstreamPeer && child.parentPeerID == peerID {
childPeerIDs = append(childPeerIDs, childPeerID)
}
}
upstream := protocol.NoUpstream()
switch {
case peerID == hostPeerID || !hasEdge:
case edge.kind == route.UpstreamPeer:
upstream = protocol.PeerUpstream(edge.parentPeerID)
case reachable:
upstream = protocol.SfuUpstream()
}
var sfuPublicationGeneration *string
if peerID == hostPeerID {
if publicationGeneration != "" {
generation := publicationGeneration
sfuPublicationGeneration = &generation
}
} else if reachable && hasEdge && edge.kind == route.UpstreamSfu {
generation := edge.publicationGeneration
sfuPublicationGeneration = &generation
}
assignments[peerID] = protocol.ParticipantRouteAssignment{
Upstream: upstream,
ChildPeerIDs: childPeerIDs,
SfuPublicationGeneration: sfuPublicationGeneration,
}
}
return assignments
}
// broadcastActive sends to the host first, then viewers in store order,
// with one routesChanged call at the end.
func (r *router) broadcastActive(roomID string, rm *roomRuntime) {
if rm.controller == nil {
return
}
snapshot := rm.controller.Snapshot()
if routeDebugEnabled() {
type routeEntry struct {
Child string `json:"child"`
Parent string `json:"parent"`
Usable bool `json:"usable"`
PhysicalActive bool `json:"physicalActive"`
}
routes := []routeEntry{}
for childPeerID, edge := range snapshot.UpstreamByViewer.All() {
parent := "sfu"
if edge.Kind == route.UpstreamPeer {
parent = r.debugPeer(roomID, edge.ParentPeerID)
}
routes = append(routes, routeEntry{r.debugPeer(roomID, childPeerID), parent, edge.Usable, edge.PhysicalActive})
}
r.debug(roomID, "active-topology",
"revision", snapshot.Revision,
"paused", snapshot.Paused,
"hostPublication", snapshot.HostPublication != nil && snapshot.HostPublication.PhysicalActive,
"routes", routes)
}
assignments := r.assignments(roomID, snapshot, nil, "", "")
var participants []room.ConnectedPeer
if host, ok := r.store.GetConnectedHost(roomID); ok {
participants = append(participants, host)
}
participants = append(participants, r.store.GetConnectedViewers(roomID)...)
for _, participant := range participants {
r.hooks.sendToSession(participant.SessionID, protocol.RouteUpdateActiveMessage{
Type: "route-update",
Revision: protocol.Int(snapshot.Revision),
Phase: "active",
Assignment: assignmentFor(assignments, participant.PeerID),
})
}
if r.hooks.routesChanged != nil {
r.hooks.routesChanged(roomID)
}
}
func (r *router) pathIsPhysical(roomID string, snapshot route.RouteSnapshot, childPeerID string) bool {
rm, _ := r.rooms.Get(roomID)
if rm == nil || rm.hostPeerID == "" {
return false
}
hostPeerID := rm.hostPeerID
seen := map[string]struct{}{}
current := childPeerID
for current != hostPeerID {
if _, again := seen[current]; again {
return false
}
seen[current] = struct{}{}
edge, ok := snapshot.UpstreamByViewer.Get(current)
if !ok || !edge.PhysicalActive || !edge.Usable {
return false
}
if edge.Kind == route.UpstreamSfu {
return snapshot.HostPublication != nil &&
snapshot.HostPublication.PhysicalActive &&
snapshot.HostPublication.Generation == edge.PublicationGeneration
}
if edge.ParentPeerID == hostPeerID {
return true
}
current = edge.ParentPeerID
}
return true
}
// childForPair returns an empty string when neither peer is the other's child.
func childForPair(snapshot route.RouteSnapshot, firstPeerID, secondPeerID string) string {
if first, ok := snapshot.UpstreamByViewer.Get(firstPeerID); ok &&
first.Kind == route.UpstreamPeer && first.ParentPeerID == secondPeerID {
return firstPeerID
}
if second, ok := snapshot.UpstreamByViewer.Get(secondPeerID); ok &&
second.Kind == route.UpstreamPeer && second.ParentPeerID == firstPeerID {
return secondPeerID
}
return ""
}
func (r *router) connectedPeer(roomID, peerID string) (room.ConnectedPeer, bool) {
if host, ok := r.store.GetConnectedHost(roomID); ok && host.PeerID == peerID {
return host, true
}
return r.store.GetConnectedViewer(roomID, peerID)
}
func (r *router) debugPeer(roomID, peerID string) string {
rm, _ := r.rooms.Get(roomID)
if rm != nil && peerID == rm.hostPeerID {
return "host"
}
if rm != nil && rm.controller != nil {
return rm.controller.DiagnosticParticipantLabel(peerID)
}
return "viewer-unknown"
}
// qualitySample is QualitySampleReference; kind selects the peer or sfu
// fields.
type qualitySample struct {
kind route.UpstreamKind
childPeerID string
routeRevision int64
connectionID string
publicationGeneration string
hostSessionID string
}
// qualityCopyContext is QualityCopyContext; operationReason and
// candidateTransition are "" where the TS had null.
type qualityCopyContext struct {
committedCopies int
candidateReservedCopies int
possibleCopies int
endpointCapacity int
operationReason route.DemandReason
candidateTransition route.TransitionKind
sampleRole string
}
// kv is the `...copyContext` spread of the debug events.
func (c qualityCopyContext) kv() []any {
return []any{
"committedCopies", c.committedCopies,
"candidateReservedCopies", c.candidateReservedCopies,
"possibleCopies", c.possibleCopies,
"endpointCapacity", c.endpointCapacity,
"operationReason", nullString(string(c.operationReason)),
"candidateTransition", nullString(string(c.candidateTransition)),
"sampleRole", c.sampleRole,
}
}
// qualityCopyContext uses counts without depending on iteration order.
func (r *router) qualityCopyContext(
snapshot route.RouteSnapshot,
hostPeerID string,
observedPeerID string,
endpointCapacity int,
sample qualitySample,
) qualityCopyContext {
committedCopies := 0
for _, edge := range snapshot.UpstreamByViewer.Values() {
if edge.Kind == route.UpstreamPeer && edge.ParentPeerID == observedPeerID && edge.PhysicalActive {
committedCopies++
}
}
if observedPeerID == hostPeerID && snapshot.HostPublication != nil && snapshot.HostPublication.PhysicalActive {
committedCopies++
}
operation := snapshot.Operation
var current *route.CurrentAttempt
var currentPlan *route.CandidatePlan
if operation != nil {
current = operation.Current
if current != nil && operation.Cursor >= 0 && operation.Cursor < len(operation.Candidates) {
plan := operation.Candidates[operation.Cursor]
currentPlan = &plan
}
}
candidateReservedCopies := 0
if current != nil {
if current.Tuple.Kind == route.UpstreamPeer && current.Tuple.ParentPeerID == observedPeerID {
candidateReservedCopies = 1
} else if current.Tuple.Kind == route.UpstreamSfu && observedPeerID == hostPeerID &&
current.Tuple.Publication != route.PublicationReuse {
candidateReservedCopies = 1
}
}
candidateSample := false
if current != nil {
if sample.kind == route.UpstreamPeer {
candidateSample = current.Tuple.Kind == route.UpstreamPeer &&
operation.ChildPeerID == sample.childPeerID &&
current.Tuple.ParentPeerID == observedPeerID &&
current.Revision == sample.routeRevision &&
current.ConnectionID == sample.connectionID
} else {
candidateSample = current.Tuple.Kind == route.UpstreamSfu &&
observedPeerID == hostPeerID &&
current.Revision == sample.routeRevision
}
}
activeSample := false
if sample.kind == route.UpstreamPeer {
activeEdge, hasActive := snapshot.UpstreamByViewer.Get(sample.childPeerID)
activeSample = sample.routeRevision == snapshot.Revision &&
hasActive && activeEdge.Kind == route.UpstreamPeer &&
activeEdge.PhysicalActive && activeEdge.Usable &&
activeEdge.ParentPeerID == observedPeerID &&
activeEdge.ConnectionID == sample.connectionID
} else {
publication := snapshot.HostPublication
activeSample = sample.routeRevision == snapshot.Revision &&
publication != nil && publication.PhysicalActive && publication.Usable &&
publication.Generation == sample.publicationGeneration &&
publication.HostSessionID == sample.hostSessionID
}
context := qualityCopyContext{
committedCopies: committedCopies,
candidateReservedCopies: candidateReservedCopies,
possibleCopies: committedCopies + candidateReservedCopies,
endpointCapacity: endpointCapacity,
sampleRole: "unknown",
}
if operation != nil {
context.operationReason = operation.Reason
}
if currentPlan != nil {
context.candidateTransition = currentPlan.EndpointTransition.Kind
}
if candidateSample {
context.sampleRole = "candidate"
} else if activeSample {
context.sampleRole = "active"
}
return context
}
func (r *router) debugTuple(roomID string, tuple route.CandidateTuple) string {
if tuple.Kind == route.UpstreamPeer {
label := "p2p:" + r.debugPeer(roomID, tuple.ParentPeerID)
if tuple.Regenerate {
label += ":regenerate"
}
return label
}
return "sfu:" + string(tuple.Publication)
}
// debug emits one sanitised slog event. Details are key/value
// pairs; never pass raw peer, session or connection IDs (use debugPeer).
func (r *router) debug(roomID, event string, details ...any) {
if !routeDebugEnabled() {
return
}
args := make([]any, 0, 4+len(details))
args = append(args, "event", event, "roomId", roomID)
args = append(args, details...)
slog.Info("piik-route", args...)
}
// sendRoomError notifies only the connected host.
func (r *router) sendRoomError(roomID, code, message string) {
if host, ok := r.store.GetConnectedHost(roomID); ok {
r.hooks.sendToSession(host.SessionID, protocol.ErrorMessage{Type: "error", Code: code, Message: message})
}
}
func (r *router) sendViewerRouteStatus(roomID, viewerPeerID string, message protocol.RouteStatusMessage) {
if viewer, ok := r.store.GetConnectedViewer(roomID, viewerPeerID); ok {
r.hooks.sendToSession(viewer.SessionID, message)
}
}
// sendRouteFailures deduplicates recipients; first occurrence wins.
func (r *router) sendRouteFailures(roomID string, failedPeerIDs []string, revision int64) {
seen := map[string]struct{}{}
for _, peerID := range failedPeerIDs {
if _, duplicate := seen[peerID]; duplicate {
continue
}
seen[peerID] = struct{}{}
r.sendViewerRouteStatus(roomID, peerID, protocol.RouteStatusMessage{
Type: "route-status",
Revision: protocol.Int(revision),
State: "failed",
Reason: "route-exhausted",
})
}
}
func (r *router) nowPtr() *int64 {
now := r.now()
return &now
}
func emptyAssignment() protocol.ParticipantRouteAssignment {
return protocol.ParticipantRouteAssignment{
Upstream: protocol.NoUpstream(),
ChildPeerIDs: []string{},
SfuPublicationGeneration: nil,
}
}
func assignmentFor(assignments map[string]protocol.ParticipantRouteAssignment, peerID string) protocol.ParticipantRouteAssignment {
if assignment, ok := assignments[peerID]; ok {
return assignment
}
return emptyAssignment()
}
// opaqueID encodes 16 random bytes as base64url without padding.
func opaqueID() string {
value := make([]byte, 16)
// crypto/rand.Read never fails; it panics if the operating system source does.
_, _ = rand.Read(value)
return base64.RawURLEncoding.EncodeToString(value)
}
func sampleTimestamp(value *protocol.Num) *int64 {
if value == nil {
return nil
}
timestamp := int64(*value)
return &timestamp
}
func intFloat(value *protocol.Int) *float64 {
if value == nil {
return nil
}
converted := float64(*value)
return &converted
}
// optional is the debug value of a nullable pointer field: nil for null.
func optional[T any](value *T) any {
if value == nil {
return nil
}
return *value
}
// nullable is the `value ?? null` of an optional-and-nullable field.
func nullable[T any](value protocol.Nullable[T]) any {
if value.Value == nil {
return nil
}
return *value.Value
}
// nullString is the debug value of a string that is "" where the TS had
// null or undefined.
func nullString(value string) any {
if value == "" {
return nil
}
return value
}