Skip to content

Commit cdc3d3d

Browse files
publish new codec when subscriber need (#267)
* publish new codec when subscriber need Co-authored-by: David Zhao <[email protected]>
1 parent 9df9cbe commit cdc3d3d

File tree

6 files changed

+191
-74
lines changed

6 files changed

+191
-74
lines changed

.changeset/loud-buses-swim.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'livekit-client': patch
3+
---
4+
5+
Start publishing backwards-compatible codec only when subscribers need it

example/sample.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
import {
2-
ConnectionQuality, ConnectionState, DataPacket_Kind,
3-
LocalParticipant, LogLevel, MediaDeviceFailure,
2+
ConnectionQuality,
3+
ConnectionState,
4+
DataPacket_Kind,
5+
LocalParticipant,
6+
LogLevel,
7+
MediaDeviceFailure,
48
Participant,
59
ParticipantEvent,
6-
RemoteParticipant, RemoteTrackPublication, RemoteVideoTrack, Room,
10+
RemoteParticipant,
11+
RemoteTrackPublication,
12+
RemoteVideoTrack,
13+
Room,
714
RoomConnectOptions,
815
RoomEvent,
9-
RoomOptions, setLogLevel,
16+
RoomOptions,
17+
setLogLevel,
1018
Track,
1119
TrackPublication,
12-
VideoCaptureOptions, VideoCodec, VideoPresets, VideoQuality
13-
} from '../src/index'
20+
VideoCaptureOptions,
21+
VideoCodec,
22+
VideoPresets,
23+
VideoQuality,
24+
} from '../src/index';
1425

1526
const $ = (id: string) => document.getElementById(id);
1627

src/proto/livekit_rtc.ts

+12
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ export interface AddTrackRequest {
209209
source: TrackSource;
210210
layers: VideoLayer[];
211211
simulcastCodecs: SimulcastCodec[];
212+
/** server ID of track, publish new codec to exist track */
213+
sid: string;
212214
}
213215

214216
export interface TrickleRequest {
@@ -993,6 +995,7 @@ function createBaseAddTrackRequest(): AddTrackRequest {
993995
source: 0,
994996
layers: [],
995997
simulcastCodecs: [],
998+
sid: '',
996999
};
9971000
}
9981001

@@ -1028,6 +1031,9 @@ export const AddTrackRequest = {
10281031
for (const v of message.simulcastCodecs) {
10291032
SimulcastCodec.encode(v!, writer.uint32(82).fork()).ldelim();
10301033
}
1034+
if (message.sid !== '') {
1035+
writer.uint32(90).string(message.sid);
1036+
}
10311037
return writer;
10321038
},
10331039

@@ -1068,6 +1074,9 @@ export const AddTrackRequest = {
10681074
case 10:
10691075
message.simulcastCodecs.push(SimulcastCodec.decode(reader, reader.uint32()));
10701076
break;
1077+
case 11:
1078+
message.sid = reader.string();
1079+
break;
10711080
default:
10721081
reader.skipType(tag & 7);
10731082
break;
@@ -1092,6 +1101,7 @@ export const AddTrackRequest = {
10921101
simulcastCodecs: Array.isArray(object?.simulcastCodecs)
10931102
? object.simulcastCodecs.map((e: any) => SimulcastCodec.fromJSON(e))
10941103
: [],
1104+
sid: isSet(object.sid) ? String(object.sid) : '',
10951105
};
10961106
},
10971107

@@ -1117,6 +1127,7 @@ export const AddTrackRequest = {
11171127
} else {
11181128
obj.simulcastCodecs = [];
11191129
}
1130+
message.sid !== undefined && (obj.sid = message.sid);
11201131
return obj;
11211132
},
11221133

@@ -1133,6 +1144,7 @@ export const AddTrackRequest = {
11331144
message.layers = object.layers?.map((e) => VideoLayer.fromPartial(e)) || [];
11341145
message.simulcastCodecs =
11351146
object.simulcastCodecs?.map((e) => SimulcastCodec.fromPartial(e)) || [];
1147+
message.sid = object.sid ?? '';
11361148
return message;
11371149
},
11381150
};

src/room/Room.ts

-8
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
108108
this.identityToSid = new Map();
109109
this.options = options || {};
110110

111-
switch (this.options?.publishDefaults?.videoCodec) {
112-
case 'av1':
113-
case 'vp9':
114-
this.options.publishDefaults.simulcast = undefined;
115-
break;
116-
default:
117-
}
118-
119111
this.options.audioCaptureDefaults = {
120112
...audioDefaults,
121113
...options?.audioCaptureDefaults,

src/room/participant/LocalParticipant.ts

+128-55
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ import RTCEngine from '../RTCEngine';
1515
import LocalAudioTrack from '../track/LocalAudioTrack';
1616
import LocalTrack from '../track/LocalTrack';
1717
import LocalTrackPublication from '../track/LocalTrackPublication';
18-
import LocalVideoTrack, {
19-
SimulcastTrackInfo,
20-
videoLayersFromEncodings,
21-
} from '../track/LocalVideoTrack';
18+
import LocalVideoTrack, { videoLayersFromEncodings } from '../track/LocalVideoTrack';
2219
import {
2320
AudioCaptureOptions,
2421
CreateLocalTracksOptions,
@@ -36,7 +33,7 @@ import { ParticipantTrackPermission, trackPermissionToProto } from './Participan
3633
import { computeVideoEncodings, mediaTrackToLocalTrack } from './publishUtils';
3734
import RemoteParticipant from './RemoteParticipant';
3835

39-
const compatibleCodecForSVC = 'vp8';
36+
const compatibleCodec = 'vp8';
4037
export default class LocalParticipant extends Participant {
4138
audioTracks: Map<string, LocalTrackPublication>;
4239

@@ -455,7 +452,6 @@ export default class LocalParticipant extends Participant {
455452
// compute encodings and layers for video
456453
let encodings: RTCRtpEncodingParameters[] | undefined;
457454
let simEncodings: RTCRtpEncodingParameters[] | undefined;
458-
let simulcastTracks: SimulcastTrackInfo[] | undefined;
459455
if (track.kind === Track.Kind.Video) {
460456
// TODO: support react native, which doesn't expose getSettings
461457
const settings = track.mediaStreamTrack.getSettings();
@@ -465,37 +461,38 @@ export default class LocalParticipant extends Participant {
465461
req.width = width ?? 0;
466462
req.height = height ?? 0;
467463
// for svc codecs, disable simulcast and use vp8 for backup codec
468-
if (
469-
track instanceof LocalVideoTrack &&
470-
(opts?.videoCodec === 'vp9' || opts?.videoCodec === 'av1')
471-
) {
472-
// set scalabilityMode to 'L3T3' by default
473-
opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3';
474-
475-
// add backup codec track
476-
const simOpts = { ...opts };
477-
simOpts.simulcast = true;
478-
simOpts.scalabilityMode = undefined;
479-
simEncodings = computeVideoEncodings(
480-
track.source === Track.Source.ScreenShare,
481-
width,
482-
height,
483-
simOpts,
484-
);
485-
const simulcastTrack = track.addSimulcastTrack(compatibleCodecForSVC, simEncodings);
486-
simulcastTracks = [simulcastTrack];
487-
req.simulcastCodecs = [
488-
{
489-
codec: opts.videoCodec,
490-
cid: track.mediaStreamTrack.id,
491-
enableSimulcastLayers: true,
492-
},
493-
{
494-
codec: simulcastTrack.codec,
495-
cid: simulcastTrack.mediaStreamTrack.id,
496-
enableSimulcastLayers: true,
497-
},
498-
];
464+
if (track instanceof LocalVideoTrack) {
465+
if (opts?.videoCodec === 'vp9' || opts?.videoCodec === 'av1') {
466+
// set scalabilityMode to 'L3T3' by default
467+
opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3';
468+
469+
// add backup codec track
470+
const simOpts = { ...opts };
471+
simOpts.simulcast = true;
472+
simOpts.scalabilityMode = undefined;
473+
simEncodings = computeVideoEncodings(
474+
track.source === Track.Source.ScreenShare,
475+
width,
476+
height,
477+
simOpts,
478+
);
479+
}
480+
481+
// set vp8 codec as backup for any other codecs
482+
if (opts.videoCodec && opts.videoCodec !== 'vp8') {
483+
req.simulcastCodecs = [
484+
{
485+
codec: opts.videoCodec,
486+
cid: track.mediaStreamTrack.id,
487+
enableSimulcastLayers: true,
488+
},
489+
{
490+
codec: compatibleCodec,
491+
cid: '',
492+
enableSimulcastLayers: true,
493+
},
494+
];
495+
}
499496
}
500497

501498
encodings = computeVideoEncodings(
@@ -541,22 +538,6 @@ export default class LocalParticipant extends Participant {
541538
track.codec = opts.videoCodec;
542539
}
543540

544-
const localTrack = track as LocalVideoTrack;
545-
if (simulcastTracks) {
546-
for await (const simulcastTrack of simulcastTracks) {
547-
const simTransceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
548-
if (simulcastTrack.encodings) {
549-
simTransceiverInit.sendEncodings = simulcastTrack.encodings;
550-
}
551-
const simTransceiver = await this.engine.publisher!.pc.addTransceiver(
552-
simulcastTrack.mediaStreamTrack,
553-
simTransceiverInit,
554-
);
555-
this.setPreferredCodec(simTransceiver, localTrack.kind, simulcastTrack.codec);
556-
localTrack.setSimulcastTrackSender(simulcastTrack.codec, simTransceiver.sender);
557-
}
558-
}
559-
560541
this.engine.negotiate();
561542

562543
// store RTPSender
@@ -574,6 +555,91 @@ export default class LocalParticipant extends Participant {
574555
return publication;
575556
}
576557

558+
/** @internal
559+
* publish additional codec to existing track
560+
*/
561+
async publishAdditionalCodecForTrack(
562+
track: LocalTrack | MediaStreamTrack,
563+
videoCodec: VideoCodec,
564+
options?: TrackPublishOptions,
565+
) {
566+
const opts: TrackPublishOptions = {
567+
...this.roomOptions?.publishDefaults,
568+
...options,
569+
};
570+
// clear scalabilityMode setting for backup codec
571+
opts.scalabilityMode = undefined;
572+
opts.videoCodec = videoCodec;
573+
// is it not published? if so skip
574+
let existingPublication: LocalTrackPublication | undefined;
575+
this.tracks.forEach((publication) => {
576+
if (!publication.track) {
577+
return;
578+
}
579+
if (publication.track === track) {
580+
existingPublication = <LocalTrackPublication>publication;
581+
}
582+
});
583+
if (!existingPublication) {
584+
throw new TrackInvalidError('track is not published');
585+
}
586+
587+
if (!(track instanceof LocalVideoTrack)) {
588+
throw new TrackInvalidError('track is not a video track');
589+
}
590+
591+
const settings = track.mediaStreamTrack.getSettings();
592+
const width = settings.width ?? track.dimensions?.width;
593+
const height = settings.height ?? track.dimensions?.height;
594+
595+
const encodings = computeVideoEncodings(
596+
track.source === Track.Source.ScreenShare,
597+
width,
598+
height,
599+
opts,
600+
);
601+
const simulcastTrack = track.addSimulcastTrack(opts.videoCodec, encodings);
602+
const req = AddTrackRequest.fromPartial({
603+
cid: simulcastTrack.mediaStreamTrack.id,
604+
type: Track.kindToProto(track.kind),
605+
muted: track.isMuted,
606+
source: Track.sourceToProto(track.source),
607+
sid: track.sid,
608+
simulcastCodecs: [
609+
{
610+
codec: opts.videoCodec,
611+
cid: simulcastTrack.mediaStreamTrack.id,
612+
enableSimulcastLayers: opts.simulcast,
613+
},
614+
],
615+
});
616+
req.layers = videoLayersFromEncodings(req.width, req.height, encodings);
617+
618+
if (!this.engine || this.engine.isClosed) {
619+
throw new UnexpectedConnectionState('cannot publish track when not connected');
620+
}
621+
622+
const ti = await this.engine.addTrack(req);
623+
624+
if (!this.engine.publisher) {
625+
throw new UnexpectedConnectionState('publisher is closed');
626+
}
627+
const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
628+
if (encodings) {
629+
transceiverInit.sendEncodings = encodings;
630+
}
631+
// addTransceiver for react-native is async. web is synchronous, but await won't effect it.
632+
const transceiver = await this.engine.publisher.pc.addTransceiver(
633+
simulcastTrack.mediaStreamTrack,
634+
transceiverInit,
635+
);
636+
this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
637+
track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
638+
639+
this.engine.negotiate();
640+
log.debug(`published ${opts.videoCodec} for track ${track.sid}`, { encodings, trackInfo: ti });
641+
}
642+
577643
unpublishTrack(
578644
track: LocalTrack | MediaStreamTrack,
579645
stopOnUnpublish?: boolean,
@@ -761,7 +827,7 @@ export default class LocalParticipant extends Participant {
761827
this.onTrackMuted(track, track.isMuted);
762828
};
763829

764-
private handleSubscribedQualityUpdate = (update: SubscribedQualityUpdate) => {
830+
private handleSubscribedQualityUpdate = async (update: SubscribedQualityUpdate) => {
765831
if (!this.roomOptions?.dynacast) {
766832
return;
767833
}
@@ -774,7 +840,14 @@ export default class LocalParticipant extends Participant {
774840
return;
775841
}
776842
if (update.subscribedCodecs.length > 0) {
777-
pub.videoTrack?.setPublishingCodecs(update.subscribedCodecs);
843+
if (!pub.videoTrack) {
844+
return;
845+
}
846+
const newCodecs = await pub.videoTrack.setPublishingCodecs(update.subscribedCodecs);
847+
for await (const codec of newCodecs) {
848+
log.debug(`publish ${codec} for ${pub.videoTrack.sid}`);
849+
await this.publishAdditionalCodecForTrack(pub.videoTrack, codec, pub.options);
850+
}
778851
} else if (update.subscribedQualities.length > 0) {
779852
pub.videoTrack?.setPublishingLayers(update.subscribedQualities);
780853
}

0 commit comments

Comments
 (0)