Skip to content

Commit 0c0e5cc

Browse files
cnderrauberlukasIOdavidzhao
authored
Support track level stereo and red setting (#470)
* Support track level stereo and red setting * check stereo is already exist * move options calc to publishTrack * prettier * Move all logic into `publishTrack` and set explicit defaults (#474) * Move all logic into and set explicit defaults * remove unnecessary null check * fix check * only log warning when red or dtx aren't set * not disable red in stereo by default * Update src/room/participant/LocalParticipant.ts Co-authored-by: David Zhao <[email protected]> Co-authored-by: lukasIO <[email protected]> Co-authored-by: David Zhao <[email protected]>
1 parent 859e3bf commit 0c0e5cc

File tree

6 files changed

+120
-13
lines changed

6 files changed

+120
-13
lines changed

.changeset/fifty-chefs-smoke.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'livekit-client': patch
3+
---
4+
5+
Add stereo and red support for track level

src/proto/livekit_rtc.ts

+23
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ export interface AddTrackRequest {
195195
simulcastCodecs: SimulcastCodec[];
196196
/** server ID of track, publish new codec to exist track */
197197
sid: string;
198+
stereo: boolean;
199+
/** true if RED (Redundant Encoding) is disabled for audio */
200+
disableRed: boolean;
198201
}
199202

200203
export interface TrickleRequest {
@@ -1078,6 +1081,8 @@ function createBaseAddTrackRequest(): AddTrackRequest {
10781081
layers: [],
10791082
simulcastCodecs: [],
10801083
sid: "",
1084+
stereo: false,
1085+
disableRed: false,
10811086
};
10821087
}
10831088

@@ -1116,6 +1121,12 @@ export const AddTrackRequest = {
11161121
if (message.sid !== "") {
11171122
writer.uint32(90).string(message.sid);
11181123
}
1124+
if (message.stereo === true) {
1125+
writer.uint32(96).bool(message.stereo);
1126+
}
1127+
if (message.disableRed === true) {
1128+
writer.uint32(104).bool(message.disableRed);
1129+
}
11191130
return writer;
11201131
},
11211132

@@ -1159,6 +1170,12 @@ export const AddTrackRequest = {
11591170
case 11:
11601171
message.sid = reader.string();
11611172
break;
1173+
case 12:
1174+
message.stereo = reader.bool();
1175+
break;
1176+
case 13:
1177+
message.disableRed = reader.bool();
1178+
break;
11621179
default:
11631180
reader.skipType(tag & 7);
11641181
break;
@@ -1182,6 +1199,8 @@ export const AddTrackRequest = {
11821199
? object.simulcastCodecs.map((e: any) => SimulcastCodec.fromJSON(e))
11831200
: [],
11841201
sid: isSet(object.sid) ? String(object.sid) : "",
1202+
stereo: isSet(object.stereo) ? Boolean(object.stereo) : false,
1203+
disableRed: isSet(object.disableRed) ? Boolean(object.disableRed) : false,
11851204
};
11861205
},
11871206

@@ -1206,6 +1225,8 @@ export const AddTrackRequest = {
12061225
obj.simulcastCodecs = [];
12071226
}
12081227
message.sid !== undefined && (obj.sid = message.sid);
1228+
message.stereo !== undefined && (obj.stereo = message.stereo);
1229+
message.disableRed !== undefined && (obj.disableRed = message.disableRed);
12091230
return obj;
12101231
},
12111232

@@ -1222,6 +1243,8 @@ export const AddTrackRequest = {
12221243
message.layers = object.layers?.map((e) => VideoLayer.fromPartial(e)) || [];
12231244
message.simulcastCodecs = object.simulcastCodecs?.map((e) => SimulcastCodec.fromPartial(e)) || [];
12241245
message.sid = object.sid ?? "";
1246+
message.stereo = object.stereo ?? false;
1247+
message.disableRed = object.disableRed ?? false;
12251248
return message;
12261249
},
12271250
};

src/room/PCTransport.ts

+49-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export default class PCTransport {
2222

2323
trackBitrates: TrackBitrateInfo[] = [];
2424

25+
remoteStereoMids: string[] = [];
26+
2527
onOffer?: (offer: RTCSessionDescriptionInit) => void;
2628

2729
constructor(config?: RTCConfiguration) {
@@ -40,6 +42,9 @@ export default class PCTransport {
4042
}
4143

4244
async setRemoteDescription(sd: RTCSessionDescriptionInit): Promise<void> {
45+
if (sd.type === 'offer') {
46+
this.remoteStereoMids = extractStereoTracksFromOffer(sd);
47+
}
4348
await this.pc.setRemoteDescription(sd);
4449

4550
this.pendingCandidates.forEach((candidate) => {
@@ -101,7 +106,7 @@ export default class PCTransport {
101106
const sdpParsed = parse(offer.sdp ?? '');
102107
sdpParsed.media.forEach((media) => {
103108
if (media.type === 'audio') {
104-
ensureAudioNack(media);
109+
ensureAudioNackAndStereo(media, []);
105110
} else if (media.type === 'video') {
106111
// mung sdp for codec bitrate setting that can't apply by sendEncoding
107112
this.trackBitrates.some((trackbr): boolean => {
@@ -154,7 +159,7 @@ export default class PCTransport {
154159
const sdpParsed = parse(answer.sdp ?? '');
155160
sdpParsed.media.forEach((media) => {
156161
if (media.type === 'audio') {
157-
ensureAudioNack(media);
162+
ensureAudioNackAndStereo(media, this.remoteStereoMids);
158163
}
159164
});
160165
await this.setMungedLocalDescription(answer, write(sdpParsed));
@@ -203,13 +208,14 @@ export default class PCTransport {
203208
}
204209
}
205210

206-
function ensureAudioNack(
211+
function ensureAudioNackAndStereo(
207212
media: {
208213
type: string;
209214
port: number;
210215
protocol: string;
211216
payloads?: string | undefined;
212217
} & MediaDescription,
218+
stereoMids: string[],
213219
) {
214220
// found opus codec to add nack fb
215221
let opusPayload = 0;
@@ -233,5 +239,45 @@ function ensureAudioNack(
233239
type: 'nack',
234240
});
235241
}
242+
243+
if (stereoMids.includes(media.mid!)) {
244+
media.fmtp.some((fmtp): boolean => {
245+
if (fmtp.payload === opusPayload) {
246+
if (!fmtp.config.includes('stereo=1')) {
247+
fmtp.config += ';stereo=1';
248+
}
249+
return true;
250+
}
251+
return false;
252+
});
253+
}
236254
}
237255
}
256+
257+
function extractStereoTracksFromOffer(offer: RTCSessionDescriptionInit): string[] {
258+
const stereoMids: string[] = [];
259+
const sdpParsed = parse(offer.sdp ?? '');
260+
let opusPayload = 0;
261+
sdpParsed.media.forEach((media) => {
262+
if (media.type === 'audio') {
263+
media.rtp.some((rtp): boolean => {
264+
if (rtp.codec === 'opus') {
265+
opusPayload = rtp.payload;
266+
return true;
267+
}
268+
return false;
269+
});
270+
271+
media.fmtp.some((fmtp): boolean => {
272+
if (fmtp.payload === opusPayload) {
273+
if (fmtp.config.includes('sprop-stereo=1')) {
274+
stereoMids.push(media.mid!);
275+
}
276+
return true;
277+
}
278+
return false;
279+
});
280+
}
281+
});
282+
return stereoMids;
283+
}

src/room/defaults.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ import {
1212
export const publishDefaults: TrackPublishDefaults = {
1313
audioBitrate: AudioPresets.music.maxBitrate,
1414
dtx: true,
15+
red: true,
16+
forceStereo: false,
1517
simulcast: true,
1618
screenShareEncoding: ScreenSharePresets.h1080fps15.encoding,
1719
stopMicTrackOnMute: false,
1820
videoCodec: 'vp8',
1921
backupCodec: { codec: 'vp8', encoding: VideoPresets.h540.encoding },
20-
};
22+
} as const;
2123

2224
export const audioDefaults: AudioCaptureOptions = {
2325
autoGainControl: true,

src/room/participant/LocalParticipant.ts

+30-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'webrtc-adapter';
22
import log from '../../logger';
3-
import type { RoomOptions } from '../../options';
3+
import type { InternalRoomOptions } from '../../options';
44
import {
55
DataPacket,
66
DataPacket_Kind,
@@ -66,10 +66,10 @@ export default class LocalParticipant extends Participant {
6666
private allParticipantsAllowedToSubscribe: boolean = true;
6767

6868
// keep a pointer to room options
69-
private roomOptions?: RoomOptions;
69+
private roomOptions: InternalRoomOptions;
7070

7171
/** @internal */
72-
constructor(sid: string, identity: string, engine: RTCEngine, options: RoomOptions) {
72+
constructor(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions) {
7373
super(sid, identity);
7474
this.audioTracks = new Map();
7575
this.videoTracks = new Map();
@@ -404,11 +404,6 @@ export default class LocalParticipant extends Participant {
404404
track: LocalTrack | MediaStreamTrack,
405405
options?: TrackPublishOptions,
406406
): Promise<LocalTrackPublication> {
407-
const opts: TrackPublishOptions = {
408-
...this.roomOptions?.publishDefaults,
409-
...options,
410-
};
411-
412407
// convert raw media track into audio or video track
413408
if (track instanceof MediaStreamTrack) {
414409
switch (track.kind) {
@@ -423,6 +418,30 @@ export default class LocalParticipant extends Participant {
423418
}
424419
}
425420

421+
const isStereo =
422+
options?.forceStereo ||
423+
('channelCount' in track.mediaStreamTrack.getSettings() &&
424+
// @ts-ignore `channelCount` on getSettings() is currently only available for Safari, but is generally the best way to determine a stereo track https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings/channelCount
425+
track.mediaStreamTrack.getSettings().channelCount === 2) ||
426+
track.mediaStreamTrack.getConstraints().channelCount === 2;
427+
428+
// disable dtx for stereo track if not enabled explicitly
429+
if (isStereo) {
430+
if (!options) {
431+
options = {};
432+
}
433+
if (options.dtx === undefined) {
434+
log.info(
435+
`Opus DTX will be disabled for stereo tracks by default. Enable them explicitly to make it work.`,
436+
);
437+
}
438+
options.dtx ??= false;
439+
}
440+
const opts: TrackPublishOptions = {
441+
...this.roomOptions.publishDefaults,
442+
...options,
443+
};
444+
426445
// is it already published? if so skip
427446
let existingPublication: LocalTrackPublication | undefined;
428447
this.tracks.forEach((publication) => {
@@ -486,7 +505,9 @@ export default class LocalParticipant extends Participant {
486505
type: Track.kindToProto(track.kind),
487506
muted: track.isMuted,
488507
source: Track.sourceToProto(track.source),
489-
disableDtx: !(opts?.dtx ?? true),
508+
disableDtx: !(opts.dtx ?? true),
509+
stereo: isStereo,
510+
disableRed: !(opts.red ?? true),
490511
});
491512

492513
// compute encodings and layers for video

src/room/track/options.ts

+10
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ export interface TrackPublishDefaults {
3232
*/
3333
dtx?: boolean;
3434

35+
/**
36+
* red (Redundant Audio Data), defaults to true
37+
*/
38+
red?: boolean;
39+
40+
/**
41+
* stereo audio track. defaults determined by capture channel count.
42+
*/
43+
forceStereo?: boolean;
44+
3545
/**
3646
* use simulcast, defaults to true.
3747
* When using simulcast, LiveKit will publish up to three versions of the stream

0 commit comments

Comments
 (0)