From d0fe8b6e6e45e78d3fd6f03a0f406ee0db064b08 Mon Sep 17 00:00:00 2001 From: Tommaso Barbugli Date: Tue, 29 Sep 2026 12:15:27 +0200 Subject: [PATCH] Add Call.UseSFU to join an SFU without the coordinator UseSFU takes SFU credentials obtained elsewhere, for example from a coordinator JoinCall made by the caller or a locally running SFU, and joins it directly. Co-Authored-By: Claude Opus 5.5 (1M context) --- call.go | 20 ++++++++++++++++++++ use_sfu_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 use_sfu_test.go diff --git a/call.go b/call.go index 21fee4e..771e076 100644 --- a/call.go +++ b/call.go @@ -735,6 +735,26 @@ func awaitMigrationCompleteOn(client *signal.Client) *event.EventAwaiter[*sfu_ev }) } +// UseSFU points the call straight at an SFU with credentials the caller already holds, so +// Join skips the coordinator's join-call request. +// +// It is for development and tests against a locally running SFU, where there is no +// coordinator to hand out an SFU and a token. Production calls get both from the +// coordinator and should not use it. Call it before Join. Reconnects keep using the same +// credentials, since there is no coordinator to ask for new ones. +func (c *Call) UseSFU(cred models.Credentials) { + c.GetCred = func(bool, string) (models.Credentials, error) { return cred, nil } + // The reconnect and migration paths read the SFU they were on from here. + c.coordinatorState.Store(&CallState{ + Url: cred.Server.URL, + Token: cred.Token, + WebsocketUrl: cred.Server.WsEndpoint, + EdgeName: cred.Server.EdgeName, + }) + c.getPeer().client.Store(signal.NewClient(cred, c, c.signalOptions()...)) + c.SetCredentials(cred) +} + func (c *Call) SetCredentials(cred models.Credentials) { c.logger.Debugf("setting credentials: %#+v", cred) state := c.coordinatorState.Load() diff --git a/use_sfu_test.go b/use_sfu_test.go new file mode 100644 index 0000000..3c6dbfe --- /dev/null +++ b/use_sfu_test.go @@ -0,0 +1,40 @@ +package rtc + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/GetStream/getstream-go-webrtc/internal/testutil" +) + +// TestUseSFUJoinsWithoutTheCoordinator joins a call through the public API with credentials +// for an SFU the caller already knows, the way a test against a local SFU does. +func TestUseSFUJoinsWithoutTheCoordinator(t *testing.T) { + t.Parallel() + + sfu := testutil.NewFakeSFU() + defer sfu.Close() + + const userID = "direct-user" + token, err := testutil.GenerateToken("test-api-key", "test-api-secret", userID, time.Hour) + require.NoError(t, err) + client, err := NewClient(token.APIKey, User{ID: userID}, StaticToken(token.Token), + WithoutCoordinatorWS()) + require.NoError(t, err) + + call := client.Call(testutil.DefaultCallType, "direct-call") + call.UseSFU(fakeSFUCredentials(sfu, "local-sfu", "sfu-token")) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _, err = call.Join(ctx) + require.NoError(t, err) + t.Cleanup(func() { _ = call.Leave("test over") }) + + join := nextJoinRequest(t, sfu) + require.Equal(t, "sfu-token", join.GetToken(), "the SFU gets the token the caller supplied") + require.Equal(t, "local-sfu", call.GetState().EdgeName, "reconnects know which SFU they were on") +}