From 9445fb3cf3d0dde13e8a872b4f091eabbe84bb26 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Sun, 28 Aug 2022 16:41:30 -0700 Subject: [PATCH 1/2] Fix issue with older browsers erroring upon setCodecPreferences On Chrome 96, using setCodecPreferences in the way that we do causes the browser to fail on setLocalDescription, even for the offer that it had created. --- package.json | 2 ++ src/room/PCTransport.ts | 69 ++++++++++++++++++++++++++--------------- src/room/RTCEngine.ts | 24 +++++++++++--- src/room/errors.ts | 14 ++++++--- src/room/utils.ts | 42 +++++++++++++++++++++++++ yarn.lock | 10 ++++++ 6 files changed, 128 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index fa4ab6aec4..822aa0f807 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "sdp-transform": "^2.14.1", "ts-debounce": "^4.0.0", "typed-emitter": "^2.1.0", + "ua-parser-js": "^1.0.2", "webrtc-adapter": "^8.1.1" }, "devDependencies": { @@ -53,6 +54,7 @@ "@rollup/plugin-node-resolve": "13.3.0", "@types/jest": "28.1.7", "@types/sdp-transform": "2.4.5", + "@types/ua-parser-js": "^0.7.36", "@types/ws": "8.5.3", "@typescript-eslint/eslint-plugin": "5.33.1", "@typescript-eslint/parser": "5.33.1", diff --git a/src/room/PCTransport.ts b/src/room/PCTransport.ts index 8e10ef65a7..1c77a26565 100644 --- a/src/room/PCTransport.ts +++ b/src/room/PCTransport.ts @@ -1,6 +1,7 @@ -import { debounce } from 'ts-debounce'; import { MediaDescription, parse, write } from 'sdp-transform'; +import { debounce } from 'ts-debounce'; import log from '../logger'; +import { NegotiationError } from './errors'; /** @internal */ interface TrackBitrateInfo { @@ -54,8 +55,16 @@ export default class PCTransport { } // debounced negotiate interface - negotiate = debounce(() => { - this.createAndSendOffer(); + negotiate = debounce((onError?: (e: Error) => void) => { + try { + this.createAndSendOffer(); + } catch (e) { + if (onError) { + onError(e as Error); + } else { + throw e; + } + } }, 100); async createAndSendOffer(options?: RTCOfferOptions) { @@ -135,17 +144,8 @@ export default class PCTransport { }); this.trackBitrates = []; - const originalSdp = offer.sdp; - try { - offer.sdp = write(sdpParsed); - await this.pc.setLocalDescription(offer); - } catch (e: unknown) { - log.warn('not able to set desired local description, falling back to unmodified offer', { - error: e, - }); - offer.sdp = originalSdp; - await this.pc.setLocalDescription(offer); - } + + await this.setMungedLocalDescription(offer, write(sdpParsed)); this.onOffer(offer); } @@ -157,17 +157,7 @@ export default class PCTransport { ensureAudioNack(media); } }); - const originalSdp = answer.sdp; - try { - answer.sdp = write(sdpParsed); - await this.pc.setLocalDescription(answer); - } catch (e: unknown) { - log.warn('not able to set desired local description, falling back to unmodified answer', { - error: e, - }); - answer.sdp = originalSdp; - await this.pc.setLocalDescription(answer); - } + await this.setMungedLocalDescription(answer, write(sdpParsed)); return answer; } @@ -182,6 +172,35 @@ export default class PCTransport { close() { this.pc.close(); } + + private async setMungedLocalDescription(sd: RTCSessionDescriptionInit, munged: string) { + const originalSdp = sd.sdp; + sd.sdp = munged; + try { + log.debug('setting munged local description'); + await this.pc.setLocalDescription(sd); + return; + } catch (e) { + log.warn(`not able to set ${sd.type}, falling back to unmodified sdp`, { + error: e, + }); + sd.sdp = originalSdp; + } + + try { + await this.pc.setLocalDescription(sd); + } catch (e) { + // this error cannot always be caught. + // If the local description has a setCodecPreferences error, this error will be uncaught + let msg = 'unknown error'; + if (e instanceof Error) { + msg = e.message; + } else if (typeof e === 'string') { + msg = e; + } + throw new NegotiationError(msg); + } + } } function ensureAudioNack( diff --git a/src/room/RTCEngine.ts b/src/room/RTCEngine.ts index 01bd6feda5..3c5d4fc505 100644 --- a/src/room/RTCEngine.ts +++ b/src/room/RTCEngine.ts @@ -21,7 +21,12 @@ import { TrackPublishedResponse, } from '../proto/livekit_rtc'; import DefaultReconnectPolicy from './DefaultReconnectPolicy'; -import { ConnectionError, TrackInvalidError, UnexpectedConnectionState } from './errors'; +import { + ConnectionError, + NegotiationError, + TrackInvalidError, + UnexpectedConnectionState, +} from './errors'; import { EngineEvent } from './events'; import PCTransport from './PCTransport'; import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy'; @@ -30,7 +35,13 @@ import type LocalVideoTrack from './track/LocalVideoTrack'; import type { SimulcastTrackInfo } from './track/LocalVideoTrack'; import type { TrackPublishOptions, VideoCodec } from './track/options'; import { Track } from './track/Track'; -import { isWeb, sleep, supportsAddTrack, supportsTransceiver } from './utils'; +import { + isWeb, + sleep, + supportsAddTrack, + supportsSetCodecPreferences, + supportsTransceiver, +} from './utils'; const lossyDataChannel = '_lossy'; const reliableDataChannel = '_reliable'; @@ -519,7 +530,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit matched.push(c); }); - if ('setCodecPreferences' in transceiver) { + if (supportsSetCodecPreferences(transceiver)) { transceiver.setCodecPreferences(matched.concat(partialMatched, unmatched)); } } @@ -900,7 +911,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit this.hasPublished = true; - this.publisher.negotiate(); + this.publisher.negotiate((e) => { + if (e instanceof NegotiationError) { + this.fullReconnectOnNext = true; + } + this.handleDisconnect('negotiation'); + }); } dataChannelForKind(kind: DataPacket_Kind, sub?: boolean): RTCDataChannel | undefined { diff --git a/src/room/errors.ts b/src/room/errors.ts index c3be9891ee..1566a21fc9 100644 --- a/src/room/errors.ts +++ b/src/room/errors.ts @@ -15,25 +15,31 @@ export class ConnectionError extends LivekitError { export class TrackInvalidError extends LivekitError { constructor(message?: string) { - super(20, message || 'Track is invalid'); + super(20, message ?? 'track is invalid'); } } export class UnsupportedServer extends LivekitError { constructor(message?: string) { - super(10, message || 'Unsupported server'); + super(10, message ?? 'unsupported server'); } } export class UnexpectedConnectionState extends LivekitError { constructor(message?: string) { - super(12, message || 'Unexpected connection state'); + super(12, message ?? 'unexpected connection state'); + } +} + +export class NegotiationError extends LivekitError { + constructor(message?: string) { + super(13, message ?? 'unable to negotiate'); } } export class PublishDataError extends LivekitError { constructor(message?: string) { - super(13, message || 'Unable to publish data'); + super(13, message ?? 'unable to publish data'); } } diff --git a/src/room/utils.ts b/src/room/utils.ts index 6aba41edc3..9dbe5b597f 100644 --- a/src/room/utils.ts +++ b/src/room/utils.ts @@ -1,3 +1,4 @@ +import UAParser from 'ua-parser-js'; import { ClientInfo, ClientInfo_SDK } from '../proto/livekit_models'; import { protocolVersion, version } from '../version'; @@ -33,6 +34,34 @@ export function supportsDynacast() { return supportsTransceiver(); } +const setCodecPreferencesVersions: { [key: string]: string } = { + Chrome: '100', + Chromium: '100', + Safari: '15', + Firefox: '100', + Edge: '100', + Brave: '1.40', +}; + +export function supportsSetCodecPreferences(transceiver: RTCRtpTransceiver): boolean { + if (!isWeb()) { + return false; + } + if (!('setCodecPreferences' in transceiver)) { + return false; + } + const uap = UAParser(); + if (!uap.browser.name || !uap.browser.version) { + // version is required + return false; + } + const v = setCodecPreferencesVersions[uap.browser.name]; + if (v) { + return compareVersions(uap.browser.version, v) >= 0; + } + return false; +} + export function isBrowserSupported() { return supportsTransceiver() || supportsAddTrack(); } @@ -56,6 +85,19 @@ export function isWeb(): boolean { return typeof document !== 'undefined'; } +export function compareVersions(v1: string, v2: string): number { + const parts1 = v1.split('.'); + const parts2 = v2.split('.'); + const k = Math.min(v1.length, v2.length); + for (let i = 0; i < k; ++i) { + const p1 = parseInt(parts1[i], 10); + const p2 = parseInt(parts2[i], 10); + if (p1 > p2) return 1; + if (p1 < p2) return -1; + } + return parts1.length == parts2.length ? 0 : parts1.length < parts2.length ? -1 : 1; +} + function roDispatchCallback(entries: ResizeObserverEntry[]) { for (const entry of entries) { (entry.target as ObservableMediaElement).handleResize(entry); diff --git a/yarn.lock b/yarn.lock index d263535c38..f11de06e2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2196,6 +2196,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/ua-parser-js@^0.7.36": + version "0.7.36" + resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190" + integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ== + "@types/ws@8.5.3": version "8.5.3" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" @@ -7047,6 +7052,11 @@ typescript@4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== +ua-parser-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775" + integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg== + unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" From af7fb4287df75ef28d828b4cbb281c3eceb9a3c1 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Sun, 28 Aug 2022 16:56:55 -0700 Subject: [PATCH 2/2] changeset --- .changeset/rude-shrimps-run.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rude-shrimps-run.md diff --git a/.changeset/rude-shrimps-run.md b/.changeset/rude-shrimps-run.md new file mode 100644 index 0000000000..1143ab573e --- /dev/null +++ b/.changeset/rude-shrimps-run.md @@ -0,0 +1,5 @@ +--- +'livekit-client': patch +--- + +Fixed compatibility with older browsers with setCodecPreferences (Chrome 96)