Skip to content

Commit 55e7b82

Browse files
lukasIOsvajunas-budrys
authored andcommitted
Replace internal instanceof checks with typeguards (livekit#1378)
1 parent f88491e commit 55e7b82

19 files changed

+167
-56
lines changed

.changeset/fair-ladybugs-divide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"livekit-client": patch
3+
---
4+
5+
Replace internal instanceof checks with typeguards

examples/demo/demo.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
supportsAV1,
3636
supportsVP9,
3737
} from '../../src/index';
38-
import { isSVCCodec } from '../../src/room/utils';
38+
import { isLocalParticipant, isRemoteTrack, isSVCCodec } from '../../src/room/utils';
3939

4040
setLogLevel(LogLevel.debug);
4141

@@ -165,7 +165,7 @@ const appActions = {
165165
.on(RoomEvent.LocalTrackPublished, (pub) => {
166166
const track = pub.track as LocalAudioTrack;
167167

168-
if (track instanceof LocalAudioTrack) {
168+
if (isLocalAudioTrack(track)) {
169169
const { calculateVolume } = createAudioAnalyser(track);
170170

171171
setInterval(() => {
@@ -495,7 +495,7 @@ window.appActions = appActions;
495495

496496
function handleChatMessage(msg: ChatMessage, participant?: LocalParticipant | RemoteParticipant) {
497497
(<HTMLTextAreaElement>$('chat')).value +=
498-
`${participant?.identity}${participant instanceof LocalParticipant ? ' (me)' : ''}: ${msg.message}\n`;
498+
`${participant?.identity}${isLocalParticipant(participant) ? ' (me)' : ''}: ${msg.message}\n`;
499499
}
500500

501501
function participantConnected(participant: Participant) {
@@ -596,7 +596,7 @@ function renderParticipant(participant: Participant, remove: boolean = false) {
596596
</div>
597597
</div>
598598
${
599-
participant instanceof RemoteParticipant
599+
!isLocalParticipant(participant)
600600
? `<div class="volume-control">
601601
<input id="volume-${identity}" type="range" min="0" max="1" step="0.1" value="1" orient="vertical" />
602602
</div>`
@@ -629,7 +629,7 @@ function renderParticipant(participant: Participant, remove: boolean = false) {
629629

630630
// update properties
631631
container.querySelector(`#name-${identity}`)!.innerHTML = participant.identity;
632-
if (participant instanceof LocalParticipant) {
632+
if (isLocalParticipant(participant)) {
633633
container.querySelector(`#name-${identity}`)!.innerHTML += ' (you)';
634634
}
635635
const micElm = container.querySelector(`#mic-${identity}`)!;
@@ -642,7 +642,7 @@ function renderParticipant(participant: Participant, remove: boolean = false) {
642642
div!.classList.remove('speaking');
643643
}
644644

645-
if (participant instanceof RemoteParticipant) {
645+
if (!isLocalParticipant(participant)) {
646646
const volumeSlider = <HTMLInputElement>container.querySelector(`#volume-${identity}`);
647647
volumeSlider.addEventListener('input', (ev) => {
648648
participant.setVolume(Number.parseFloat((ev.target as HTMLInputElement).value));
@@ -651,7 +651,7 @@ function renderParticipant(participant: Participant, remove: boolean = false) {
651651

652652
const cameraEnabled = cameraPub && cameraPub.isSubscribed && !cameraPub.isMuted;
653653
if (cameraEnabled) {
654-
if (participant instanceof LocalParticipant) {
654+
if (isLocalParticipant(participant)) {
655655
// flip
656656
videoElm.style.transform = 'scale(-1, 1)';
657657
} else if (!cameraPub?.videoTrack?.attachedElements.includes(videoElm)) {
@@ -684,7 +684,7 @@ function renderParticipant(participant: Participant, remove: boolean = false) {
684684

685685
const micEnabled = micPub && micPub.isSubscribed && !micPub.isMuted;
686686
if (micEnabled) {
687-
if (!(participant instanceof LocalParticipant)) {
687+
if (!isLocalParticipant(participant)) {
688688
// don't attach local audio
689689
audioELm.onloadeddata = () => {
690690
if (participant.joinedAt && participant.joinedAt.getTime() < startTime) {
@@ -787,7 +787,7 @@ function renderBitrate() {
787787
}
788788

789789
if (t.source === Track.Source.Camera) {
790-
if (t.videoTrack instanceof RemoteVideoTrack) {
790+
if (isRemoteTrack(t.videoTrack)) {
791791
const codecElm = container.querySelector(`#codec-${p.identity}`)!;
792792
codecElm.innerHTML = t.videoTrack.getDecoderImplementation() ?? '';
793793
}

src/e2ee/E2eeManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import type Room from '../room/Room';
77
import { ConnectionState } from '../room/Room';
88
import { DeviceUnsupportedError } from '../room/errors';
99
import { EngineEvent, ParticipantEvent, RoomEvent } from '../room/events';
10-
import LocalTrack from '../room/track/LocalTrack';
1110
import type RemoteTrack from '../room/track/RemoteTrack';
1211
import type { Track } from '../room/track/Track';
1312
import type { VideoCodec } from '../room/track/options';
1413
import { mimeTypeToVideoCodecString } from '../room/track/utils';
14+
import { isLocalTrack } from '../room/utils';
1515
import type { BaseKeyProvider } from './KeyProvider';
1616
import { E2EE_FLAG } from './constants';
1717
import { type E2EEManagerCallbacks, EncryptionEvent, KeyProviderEvent } from './events';
@@ -317,7 +317,7 @@ export class E2EEManager
317317
}
318318

319319
private setupE2EESender(track: Track, sender: RTCRtpSender) {
320-
if (!(track instanceof LocalTrack) || !sender) {
320+
if (!isLocalTrack(track) || !sender) {
321321
if (!sender) log.warn('early return because sender is not ready');
322322
return;
323323
}

src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ import {
3232
createAudioAnalyser,
3333
getEmptyAudioStreamTrack,
3434
getEmptyVideoStreamTrack,
35+
isAudioTrack,
3536
isBrowserSupported,
37+
isLocalTrack,
38+
isRemoteTrack,
39+
isVideoTrack,
3640
supportsAV1,
3741
supportsAdaptiveStream,
3842
supportsDynacast,
@@ -100,6 +104,10 @@ export {
100104
supportsDynacast,
101105
supportsVP9,
102106
Mutex,
107+
isAudioTrack,
108+
isLocalTrack,
109+
isRemoteTrack,
110+
isVideoTrack,
103111
};
104112
export type {
105113
AudioAnalyserOptions,

src/room/RTCEngine.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,14 @@ import type { Track } from './track/Track';
6464
import type { TrackPublishOptions, VideoCodec } from './track/options';
6565
import { getTrackPublicationInfo } from './track/utils';
6666
import type { LoggerOptions } from './types';
67-
import { isVideoCodec, isWeb, sleep, supportsAddTrack, supportsTransceiver } from './utils';
67+
import {
68+
isVideoCodec,
69+
isVideoTrack,
70+
isWeb,
71+
sleep,
72+
supportsAddTrack,
73+
supportsTransceiver,
74+
} from './utils';
6875

6976
const lossyDataChannel = '_lossy';
7077
const reliableDataChannel = '_reliable';
@@ -745,7 +752,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
745752
streams.push(track.mediaStream);
746753
}
747754

748-
if (track instanceof LocalVideoTrack) {
755+
if (isVideoTrack(track)) {
749756
track.codec = opts.videoCodec;
750757
}
751758

src/room/Room.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ import {
8585
getEmptyAudioStreamTrack,
8686
isBrowserSupported,
8787
isCloud,
88+
isLocalAudioTrack,
89+
isLocalParticipant,
8890
isReactNative,
91+
isRemotePub,
8992
isSafari,
9093
isWeb,
9194
supportsSetSinkId,
@@ -276,7 +279,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
276279
this.e2eeManager.on(
277280
EncryptionEvent.ParticipantEncryptionStatusChanged,
278281
(enabled, participant) => {
279-
if (participant instanceof LocalParticipant) {
282+
if (isLocalParticipant(participant)) {
280283
this.isE2EEEnabled = enabled;
281284
}
282285
this.emit(RoomEvent.ParticipantEncryptionStatusChanged, enabled, participant);
@@ -1950,7 +1953,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
19501953
private updateSubscriptions() {
19511954
for (const p of this.remoteParticipants.values()) {
19521955
for (const pub of p.videoTrackPublications.values()) {
1953-
if (pub.isSubscribed && pub instanceof RemoteTrackPublication) {
1956+
if (pub.isSubscribed && isRemotePub(pub)) {
19541957
pub.emitTrackUpdate();
19551958
}
19561959
}
@@ -2072,7 +2075,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
20722075

20732076
this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
20742077

2075-
if (pub.track instanceof LocalAudioTrack) {
2078+
if (isLocalAudioTrack(pub.track)) {
20762079
const trackIsSilent = await pub.track.checkForSilence();
20772080
if (trackIsSilent) {
20782081
this.emit(RoomEvent.LocalAudioSilenceDetected, pub);

src/room/participant/LocalParticipant.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,15 @@ import type { ChatMessage, DataPublishOptions } from '../types';
6565
import {
6666
Future,
6767
compareVersions,
68+
isAudioTrack,
6869
isE2EESimulcastSupported,
6970
isFireFox,
71+
isLocalAudioTrack,
72+
isLocalTrack,
73+
isLocalVideoTrack,
7074
isSVCCodec,
7175
isSafari17,
76+
isVideoTrack,
7277
isWeb,
7378
sleep,
7479
supportsAV1,
@@ -640,9 +645,9 @@ export default class LocalParticipant extends Participant {
640645
track.setAudioContext(this.audioContext);
641646
}
642647
track.mediaStream = stream;
643-
if (track instanceof LocalAudioTrack && audioProcessor) {
648+
if (isAudioTrack(track) && audioProcessor) {
644649
await track.setProcessor(audioProcessor);
645-
} else if (track instanceof LocalVideoTrack && videoProcessor) {
650+
} else if (isVideoTrack(track) && videoProcessor) {
646651
await track.setProcessor(videoProcessor);
647652
}
648653
return track;
@@ -717,15 +722,15 @@ export default class LocalParticipant extends Participant {
717722
options?: TrackPublishOptions,
718723
isRepublish = false,
719724
): Promise<LocalTrackPublication> {
720-
if (track instanceof LocalAudioTrack) {
725+
if (isLocalAudioTrack(track)) {
721726
track.setAudioContext(this.audioContext);
722727
}
723728

724729
await this.reconnectFuture?.promise;
725730
if (this.republishPromise && !isRepublish) {
726731
await this.republishPromise;
727732
}
728-
if (track instanceof LocalTrack && this.pendingPublishPromises.has(track)) {
733+
if (isLocalTrack(track) && this.pendingPublishPromises.has(track)) {
729734
await this.pendingPublishPromises.get(track);
730735
}
731736
let defaultConstraints: MediaTrackConstraints | undefined;
@@ -857,15 +862,15 @@ export default class LocalParticipant extends Participant {
857862

858863
private async publish(track: LocalTrack, opts: TrackPublishOptions, isStereo: boolean) {
859864
const existingTrackOfSource = Array.from(this.trackPublications.values()).find(
860-
(publishedTrack) => track instanceof LocalTrack && publishedTrack.source === track.source,
865+
(publishedTrack) => isLocalTrack(track) && publishedTrack.source === track.source,
861866
);
862867
if (existingTrackOfSource && track.source !== Track.Source.Unknown) {
863868
this.log.info(`publishing a second track with the same source: ${track.source}`, {
864869
...this.logContext,
865870
...getLogContextFromTrack(track),
866871
});
867872
}
868-
if (opts.stopMicTrackOnMute && track instanceof LocalAudioTrack) {
873+
if (opts.stopMicTrackOnMute && isAudioTrack(track)) {
869874
track.stopOnMute = true;
870875
}
871876

@@ -950,7 +955,7 @@ export default class LocalParticipant extends Participant {
950955
req.width = dims.width;
951956
req.height = dims.height;
952957
// for svc codecs, disable simulcast and use vp8 for backup codec
953-
if (track instanceof LocalVideoTrack) {
958+
if (isLocalVideoTrack(track)) {
954959
if (isSVCCodec(videoCodec)) {
955960
if (track.source === Track.Source.ScreenShare) {
956961
// vp9 svc with screenshare cannot encode multiple spatial layers
@@ -1036,7 +1041,7 @@ export default class LocalParticipant extends Participant {
10361041

10371042
track.sender = await this.engine.createSender(track, opts, encodings);
10381043

1039-
if (track instanceof LocalVideoTrack) {
1044+
if (isLocalVideoTrack(track)) {
10401045
opts.degradationPreference ??= getDefaultDegradationPreference(track);
10411046
track.setDegradationPreference(opts.degradationPreference);
10421047
}
@@ -1126,9 +1131,9 @@ export default class LocalParticipant extends Participant {
11261131
trackInfo: ti,
11271132
});
11281133

1129-
if (track instanceof LocalVideoTrack) {
1134+
if (isLocalVideoTrack(track)) {
11301135
track.startMonitor(this.engine.client);
1131-
} else if (track instanceof LocalAudioTrack) {
1136+
} else if (isLocalAudioTrack(track)) {
11321137
track.startMonitor();
11331138
}
11341139

@@ -1169,7 +1174,7 @@ export default class LocalParticipant extends Participant {
11691174
throw new TrackInvalidError('track is not published');
11701175
}
11711176

1172-
if (!(track instanceof LocalVideoTrack)) {
1177+
if (!isLocalVideoTrack(track)) {
11731178
throw new TrackInvalidError('track is not a video track');
11741179
}
11751180

@@ -1236,7 +1241,7 @@ export default class LocalParticipant extends Participant {
12361241
track: LocalTrack | MediaStreamTrack,
12371242
stopOnUnpublish?: boolean,
12381243
): Promise<LocalTrackPublication | undefined> {
1239-
if (track instanceof LocalTrack) {
1244+
if (isLocalTrack(track)) {
12401245
const publishPromise = this.pendingPublishPromises.get(track);
12411246
if (publishPromise) {
12421247
this.log.info('awaiting publish promise before attempting to unpublish', {
@@ -1303,7 +1308,7 @@ export default class LocalParticipant extends Participant {
13031308
if (this.engine.removeTrack(trackSender)) {
13041309
negotiationNeeded = true;
13051310
}
1306-
if (track instanceof LocalVideoTrack) {
1311+
if (isLocalVideoTrack(track)) {
13071312
for (const [, trackInfo] of track.simulcastCodecs) {
13081313
if (trackInfo.sender) {
13091314
if (this.engine.removeTrack(trackInfo.sender)) {
@@ -1349,9 +1354,7 @@ export default class LocalParticipant extends Participant {
13491354
tracks: LocalTrack[] | MediaStreamTrack[],
13501355
): Promise<LocalTrackPublication[]> {
13511356
const results = await Promise.all(tracks.map((track) => this.unpublishTrack(track)));
1352-
return results.filter(
1353-
(track) => track instanceof LocalTrackPublication,
1354-
) as LocalTrackPublication[];
1357+
return results.filter((track) => !!track);
13551358
}
13561359

13571360
async republishAllTracks(options?: TrackPublishOptions, restartTracks: boolean = true) {
@@ -1379,7 +1382,7 @@ export default class LocalParticipant extends Participant {
13791382
!track.isMuted &&
13801383
track.source !== Track.Source.ScreenShare &&
13811384
track.source !== Track.Source.ScreenShareAudio &&
1382-
(track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) &&
1385+
(isLocalAudioTrack(track) || isLocalVideoTrack(track)) &&
13831386
!track.isUserProvided
13841387
) {
13851388
// generally we need to restart the track before publishing, often a full reconnect
@@ -1962,7 +1965,7 @@ export default class LocalParticipant extends Participant {
19621965
this.unpublishTrack(track);
19631966
} else if (track.isUserProvided) {
19641967
await track.mute();
1965-
} else if (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) {
1968+
} else if (isLocalAudioTrack(track) || isLocalVideoTrack(track)) {
19661969
try {
19671970
if (isWeb()) {
19681971
try {
@@ -1997,7 +2000,7 @@ export default class LocalParticipant extends Participant {
19972000
...this.logContext,
19982001
...getLogContextFromTrack(track),
19992002
});
2000-
if (track instanceof LocalAudioTrack) {
2003+
if (isLocalAudioTrack(track)) {
20012004
// fall back to default device if available
20022005
await track.restartTrack({ deviceId: 'default' });
20032006
} else {
@@ -2026,7 +2029,7 @@ export default class LocalParticipant extends Participant {
20262029

20272030
// this looks overly complicated due to this object tree
20282031
if (track instanceof MediaStreamTrack) {
2029-
if (localTrack instanceof LocalAudioTrack || localTrack instanceof LocalVideoTrack) {
2032+
if (isLocalAudioTrack(localTrack) || isLocalVideoTrack(localTrack)) {
20302033
if (localTrack.mediaStreamTrack === track) {
20312034
publication = <LocalTrackPublication>pub;
20322035
}

src/room/participant/Participant.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@ import { EventEmitter } from 'events';
1111
import type TypedEmitter from 'typed-emitter';
1212
import log, { LoggerNames, type StructuredLogger, getLogger } from '../../logger';
1313
import { ParticipantEvent, TrackEvent } from '../events';
14-
import LocalAudioTrack from '../track/LocalAudioTrack';
1514
import type LocalTrackPublication from '../track/LocalTrackPublication';
16-
import RemoteAudioTrack from '../track/RemoteAudioTrack';
1715
import type RemoteTrack from '../track/RemoteTrack';
1816
import type RemoteTrackPublication from '../track/RemoteTrackPublication';
1917
import { Track } from '../track/Track';
2018
import type { TrackPublication } from '../track/TrackPublication';
2119
import { diffAttributes } from '../track/utils';
2220
import type { ChatMessage, LoggerOptions, TranscriptionSegment } from '../types';
21+
import { isAudioTrack } from '../utils';
2322

2423
export enum ConnectionQuality {
2524
Excellent = 'excellent',
@@ -317,9 +316,7 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
317316
setAudioContext(ctx: AudioContext | undefined) {
318317
this.audioContext = ctx;
319318
this.audioTrackPublications.forEach(
320-
(track) =>
321-
(track.track instanceof RemoteAudioTrack || track.track instanceof LocalAudioTrack) &&
322-
track.track.setAudioContext(ctx),
319+
(track) => isAudioTrack(track.track) && track.track.setAudioContext(ctx),
323320
);
324321
}
325322

0 commit comments

Comments
 (0)