Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve handling of incompatible published codecs #911

Merged
merged 5 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/purple-coats-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'livekit-client': patch
---

Improve handling of incompatible published codecs
3 changes: 2 additions & 1 deletion src/e2ee/E2eeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import LocalTrack from '../room/track/LocalTrack';
import type RemoteTrack from '../room/track/RemoteTrack';
import type { Track } from '../room/track/Track';
import type { VideoCodec } from '../room/track/options';
import { mimeTypeToVideoCodecString } from '../room/track/utils';
import type { BaseKeyProvider } from './KeyProvider';
import { E2EE_FLAG } from './constants';
import { type E2EEManagerCallbacks, EncryptionEvent, KeyProviderEvent } from './events';
Expand All @@ -28,7 +29,7 @@ import type {
SifTrailerMessage,
UpdateCodecMessage,
} from './types';
import { isE2EESupported, isScriptTransformSupported, mimeTypeToVideoCodecString } from './utils';
import { isE2EESupported, isScriptTransformSupported } from './utils';

/**
* @experimental
Expand Down
10 changes: 0 additions & 10 deletions src/e2ee/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { videoCodecs } from '../room/track/options';
import type { VideoCodec } from '../room/track/options';
import { ENCRYPTION_ALGORITHM } from './constants';

export function isE2EESupported() {
Expand Down Expand Up @@ -116,14 +114,6 @@ export function createE2EEKey(): Uint8Array {
return window.crypto.getRandomValues(new Uint8Array(32));
}

export function mimeTypeToVideoCodecString(mimeType: string) {
const codec = mimeType.split('/')[1].toLowerCase() as VideoCodec;
if (!videoCodecs.includes(codec)) {
throw Error(`Video codec not supported: ${codec}`);
}
return codec;
}

/**
* Ratchets a key. See
* https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
Expand Down
6 changes: 4 additions & 2 deletions src/room/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { InternalRoomConnectOptions, InternalRoomOptions } from '../options';
import DefaultReconnectPolicy from './DefaultReconnectPolicy';
import { AudioPresets, ScreenSharePresets, VideoPresets } from './track/options';
import type {
AudioCaptureOptions,
TrackPublishDefaults,
VideoCaptureOptions,
} from './track/options';
import { AudioPresets, ScreenSharePresets, VideoPresets } from './track/options';

export const defaultVideoCodec = 'vp8';

export const publishDefaults: TrackPublishDefaults = {
/**
Expand All @@ -19,7 +21,7 @@ export const publishDefaults: TrackPublishDefaults = {
simulcast: true,
screenShareEncoding: ScreenSharePresets.h1080fps15.encoding,
stopMicTrackOnMute: false,
videoCodec: 'vp8',
videoCodec: defaultVideoCodec,
backupCodec: false,
} as const;

Expand Down
90 changes: 41 additions & 49 deletions src/room/participant/LocalParticipant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
TrackUnpublishedResponse,
} from '../../proto/livekit_rtc_pb';
import type RTCEngine from '../RTCEngine';
import { defaultVideoCodec } from '../defaults';
import { DeviceUnsupportedError, TrackInvalidError, UnexpectedConnectionState } from '../errors';
import { EngineEvent, ParticipantEvent, TrackEvent } from '../events';
import LocalAudioTrack from '../track/LocalAudioTrack';
Expand All @@ -33,10 +34,11 @@ import type {
TrackPublishOptions,
VideoCaptureOptions,
} from '../track/options';
import { VideoPresets, isBackupCodec, isCodecEqual } from '../track/options';
import { VideoPresets, isBackupCodec } from '../track/options';
import {
constraintsForOptions,
mergeDefaultOptions,
mimeTypeToVideoCodecString,
screenCaptureToDisplayMediaStreamOptions,
} from '../track/utils';
import type { DataPublishOptions } from '../types';
Expand Down Expand Up @@ -629,6 +631,10 @@ export default class LocalParticipant extends Participant {
if (opts.videoCodec === 'vp9' && !supportsVP9()) {
opts.videoCodec = undefined;
}
if (opts.videoCodec === undefined) {
opts.videoCodec = defaultVideoCodec;
}
const videoCodec = opts.videoCodec;

// handle track actions
track.on(TrackEvent.Muted, this.onTrackMuted);
Expand All @@ -654,7 +660,6 @@ export default class LocalParticipant extends Participant {

// compute encodings and layers for video
let encodings: RTCRtpEncodingParameters[] | undefined;
let simEncodings: RTCRtpEncodingParameters[] | undefined;
if (track.kind === Track.Kind.Video) {
let dims: Track.Dimensions = {
width: 0,
Expand All @@ -679,50 +684,40 @@ export default class LocalParticipant extends Participant {
req.height = dims.height;
// for svc codecs, disable simulcast and use vp8 for backup codec
if (track instanceof LocalVideoTrack) {
if (isSVCCodec(opts.videoCodec)) {
if (isSVCCodec(videoCodec)) {
// vp9 svc with screenshare has problem to encode, always use L1T3 here
if (track.source === Track.Source.ScreenShare && opts.videoCodec === 'vp9') {
if (track.source === Track.Source.ScreenShare && videoCodec === 'vp9') {
opts.scalabilityMode = 'L1T3';
}
// set scalabilityMode to 'L3T3_KEY' by default
opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3_KEY';
}

req.simulcastCodecs = [
new SimulcastCodec({
codec: videoCodec,
cid: track.mediaStreamTrack.id,
}),
];

// set up backup
if (opts.videoCodec && opts.backupCodec && opts.videoCodec !== opts.backupCodec.codec) {
if (opts.backupCodec && videoCodec !== opts.backupCodec.codec) {
if (!this.roomOptions.dynacast) {
this.roomOptions.dynacast = true;
}
const simOpts = { ...opts };
simOpts.simulcast = true;
simEncodings = computeTrackBackupEncodings(track, opts.backupCodec.codec, simOpts);

req.simulcastCodecs = [
new SimulcastCodec({
codec: opts.videoCodec,
cid: track.mediaStreamTrack.id,
}),
req.simulcastCodecs.push(
new SimulcastCodec({
codec: opts.backupCodec.codec,
cid: '',
}),
];
} else if (opts.videoCodec) {
// pass codec info to sfu so it can prefer codec for the client which don't support
// setCodecPreferences
req.simulcastCodecs = [
new SimulcastCodec({
codec: opts.videoCodec,
cid: track.mediaStreamTrack.id,
}),
];
);
}
}

encodings = computeVideoEncodings(
track.source === Track.Source.ScreenShare,
dims.width,
dims.height,
req.width,
req.height,
opts,
);
req.layers = videoLayersFromEncodings(
Expand All @@ -746,30 +741,28 @@ export default class LocalParticipant extends Participant {
}

const ti = await this.engine.addTrack(req);
let primaryCodecSupported = false;
let backupCodecSupported = false;
ti.codecs.forEach((c) => {
if (isCodecEqual(c.mimeType, opts.videoCodec)) {
primaryCodecSupported = true;
} else if (opts.backupCodec && isCodecEqual(c.mimeType, opts.backupCodec.codec)) {
backupCodecSupported = true;
// server might not support the codec the client has requested, in that case, fallback
// to a supported codec
let primaryCodecMime: string | undefined;
ti.codecs.forEach((codec) => {
if (primaryCodecMime === undefined) {
primaryCodecMime = codec.mimeType;
}
});

if (req.simulcastCodecs.length > 0) {
if (!primaryCodecSupported && !backupCodecSupported) {
throw Error('cannot publish track, codec not supported by server');
}

if (!primaryCodecSupported && opts.backupCodec) {
const backupCodec = opts.backupCodec;
opts = { ...opts };
log.debug(
`primary codec ${opts.videoCodec} not supported, fallback to ${backupCodec.codec}`,
if (primaryCodecMime && track.kind === Track.Kind.Video) {
const updatedCodec = mimeTypeToVideoCodecString(primaryCodecMime);
if (updatedCodec !== videoCodec) {
log.debug('falling back to server selected codec', { codec: updatedCodec });
/* @ts-ignore */
opts.videoCodec = updatedCodec;
davidzhao marked this conversation as resolved.
Show resolved Hide resolved

// recompute encodings since bitrates/etc could have changed
encodings = computeVideoEncodings(
track.source === Track.Source.ScreenShare,
req.width,
req.height,
opts,
);
opts.videoCodec = backupCodec.codec;
opts.videoEncoding = backupCodec.encoding;
encodings = simEncodings;
}
}

Expand All @@ -783,13 +776,12 @@ export default class LocalParticipant extends Participant {
}
log.debug(`publishing ${track.kind} with encodings`, { encodings, trackInfo: ti });

// store RTPSender
track.sender = await this.engine.createSender(track, opts, encodings);

if (encodings) {
if (isFireFox() && track.kind === Track.Kind.Audio) {
/* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
livekit-server uses maxaveragebitrate=510000in the answer sdp to permit client to
livekit-server uses maxaveragebitrate=510000 in the answer sdp to permit client to
publish high quality audio track. But firefox always uses this value as the actual
bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
So the client need to modify maxaverragebitrates in answer sdp to user provided value to
Expand Down
3 changes: 2 additions & 1 deletion src/room/track/LocalVideoTrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@ export default class LocalVideoTrack extends LocalTrack {

/**
* @internal
* Sets codecs that should be publishing
* Sets codecs that should be publishing, returns new codecs that have not yet
* been published
*/
async setPublishingCodecs(codecs: SubscribedCodec[]): Promise<VideoCodec[]> {
log.debug('setting publishing codecs', {
Expand Down
7 changes: 0 additions & 7 deletions src/room/track/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,13 +301,6 @@ export function isBackupCodec(codec: string): codec is BackupVideoCodec {
return !!backupCodecs.find((backup) => backup === codec);
}

export function isCodecEqual(c1: string | undefined, c2: string | undefined): boolean {
return (
c1?.toLowerCase().replace(/audio\/|video\//y, '') ===
c2?.toLowerCase().replace(/audio\/|video\//y, '')
);
}

/**
* scalability modes for svc.
*/
Expand Down
20 changes: 15 additions & 5 deletions src/room/track/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { isSafari, sleep } from '../utils';
import { Track } from './Track';
import type {
AudioCaptureOptions,
CreateLocalTracksOptions,
ScreenShareCaptureOptions,
VideoCaptureOptions,
import {
type AudioCaptureOptions,
type CreateLocalTracksOptions,
type ScreenShareCaptureOptions,
type VideoCaptureOptions,
VideoCodec,
videoCodecs,
} from './options';
import type { AudioTrack } from './types';

Expand Down Expand Up @@ -181,3 +183,11 @@ export function screenCaptureToDisplayMediaStreamOptions(
systemAudio: options.systemAudio,
};
}

export function mimeTypeToVideoCodecString(mimeType: string) {
const codec = mimeType.split('/')[1].toLowerCase() as VideoCodec;
if (!videoCodecs.includes(codec)) {
throw Error(`Video codec not supported: ${codec}`);
}
return codec;
}
Loading