From 2d4bd8af553d81d60eed6d43eb4f875b9d0aa63a Mon Sep 17 00:00:00 2001 From: karlsonlee-livekit Date: Fri, 6 Nov 2026 10:05:13 +0000 Subject: [PATCH] rtc: disable transceiver reuse for all WebKit clients, incl. iOS (#5133) * rtc: disable transceiver reuse for all WebKit clients, incl. iOS #4474 turned off transceiver reuse on Safari because of an insertable streams issue that can corrupt E2EE tracks. The check matched only a browser name of exactly "Safari". The server's user agent parser reports iPhone Safari as "Mobile Safari", WKWebView as "Mobile Safari UI/WKWebView", and other iOS browsers as "Chrome Mobile iOS", "Firefox iOS", "Edge Mobile" or an app name. All of these run on WebKit, but they kept transceiver reuse. Match any browser name that contains "safari", and any JS SDK client on iOS. React Native, Swift and Flutter do not match. isSafari() and audio RED are not changed. Co-Authored-By: Claude Opus 5.5 * rtc: reuse isSafari() in isWebKit() Co-Authored-By: Claude Opus 5.5 * rtc: use isWebKit() for audio RED support too Co-Authored-By: Claude Fable 5.1 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: lukasIO --- pkg/rtc/clientinfo.go | 11 ++++++++-- pkg/rtc/clientinfo_test.go | 44 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/pkg/rtc/clientinfo.go b/pkg/rtc/clientinfo.go index 3dfde07ef..0bad5fdd4 100644 --- a/pkg/rtc/clientinfo.go +++ b/pkg/rtc/clientinfo.go @@ -34,6 +34,13 @@ func (c ClientInfo) isSafari() bool { return c.ClientInfo != nil && strings.EqualFold(c.ClientInfo.Browser, "safari") } +func (c ClientInfo) isWebKit() bool { + if c.isSafari() { + return true + } + return c.ClientInfo != nil && c.ClientInfo.Sdk == livekit.ClientInfo_JS && strings.EqualFold(c.ClientInfo.Os, "ios") +} + func (c ClientInfo) isGo() bool { return c.ClientInfo != nil && c.ClientInfo.Sdk == livekit.ClientInfo_GO } @@ -51,7 +58,7 @@ func (c ClientInfo) isOBS() bool { } func (c ClientInfo) SupportsAudioRED() bool { - return !c.isFirefox() && !c.isSafari() + return !c.isFirefox() && !c.isWebKit() } func (c ClientInfo) SupportsPrflxOverRelay() bool { @@ -121,7 +128,7 @@ func (c ClientInfo) SupportsAnswerDTLSRoleServer() bool { } func (c ClientInfo) SupportsTransceiverReuse() bool { - return !c.isSafari() + return !c.isWebKit() } func (c ClientInfo) HasCapability(cap livekit.ClientInfo_Capability) bool { diff --git a/pkg/rtc/clientinfo_test.go b/pkg/rtc/clientinfo_test.go index 566402430..99055a97b 100644 --- a/pkg/rtc/clientinfo_test.go +++ b/pkg/rtc/clientinfo_test.go @@ -57,3 +57,47 @@ func TestClientInfo_SupportsICETCP(t *testing.T) { require.True(t, c.SupportsICETCP()) }) } + +func TestClientInfo_WebKitFeatures(t *testing.T) { + // Browser / Os values are what the server's user agent parser sets for JS clients + testCases := []struct { + name string + sdk livekit.ClientInfo_SDK + browser string + os string + transceiverReuse bool + audioRED bool + }{ + {"macOS Safari", livekit.ClientInfo_JS, "Safari", "Mac OS X", false, false}, + {"iPadOS Safari (desktop UA)", livekit.ClientInfo_JS, "Safari", "Mac OS X", false, false}, + {"iOS Safari", livekit.ClientInfo_JS, "Mobile Safari", "iOS", false, false}, + {"iOS WKWebView", livekit.ClientInfo_JS, "Mobile Safari UI/WKWebView", "iOS", false, false}, + {"iOS Chrome", livekit.ClientInfo_JS, "Chrome Mobile iOS", "iOS", false, false}, + {"iOS Firefox", livekit.ClientInfo_JS, "Firefox iOS", "iOS", false, false}, + {"iOS Edge", livekit.ClientInfo_JS, "Edge Mobile", "iOS", false, false}, + {"iOS in-app browser", livekit.ClientInfo_JS, "Instagram", "iOS", false, false}, + {"desktop Chrome", livekit.ClientInfo_JS, "Chrome", "Mac OS X", true, true}, + {"desktop Firefox", livekit.ClientInfo_JS, "Firefox", "Windows", true, false}, + {"Android Chrome", livekit.ClientInfo_JS, "Chrome Mobile", "Android", true, true}, + {"React Native iOS", livekit.ClientInfo_REACT_NATIVE, "LiveKitExample", "ios", true, true}, + {"Swift", livekit.ClientInfo_SWIFT, "", "ios", true, true}, + {"Flutter iOS", livekit.ClientInfo_FLUTTER, "Dart", "ios", true, true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := ClientInfo{ + ClientInfo: &livekit.ClientInfo{ + Sdk: tc.sdk, + Browser: tc.browser, + Os: tc.os, + }, + } + require.Equal(t, tc.transceiverReuse, c.SupportsTransceiverReuse()) + require.Equal(t, tc.audioRED, c.SupportsAudioRED()) + }) + } + + require.True(t, ClientInfo{}.SupportsTransceiverReuse()) + require.True(t, ClientInfo{}.SupportsAudioRED()) +}