Skip to content

Commit

Permalink
Fix issue with older browsers erroring upon setCodecPreferences (#418)
Browse files Browse the repository at this point in the history
* 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.

* changeset
  • Loading branch information
davidzhao committed Aug 29, 2022
1 parent 6b748da commit 82107cf
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .changeset/rude-shrimps-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'livekit-client': patch
---

Fixed compatibility with older browsers with setCodecPreferences (Chrome 96)
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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",
Expand Down
69 changes: 44 additions & 25 deletions src/room/PCTransport.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}

Expand All @@ -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;
}

Expand All @@ -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(
Expand Down
24 changes: 20 additions & 4 deletions src/room/RTCEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -526,7 +537,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));
}
}
Expand Down Expand Up @@ -907,7 +918,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 {
Expand Down
14 changes: 10 additions & 4 deletions src/room/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}

Expand Down
42 changes: 42 additions & 0 deletions src/room/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import UAParser from 'ua-parser-js';
import { ClientInfo, ClientInfo_SDK } from '../proto/livekit_models';
import { protocolVersion, version } from '../version';

Expand Down Expand Up @@ -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();
}
Expand All @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 82107cf

Please sign in to comment.