Skip to content

Commit

Permalink
Replace internal instanceof checks with typeguards (#1378)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasIO authored Jan 22, 2025
1 parent a2f07ab commit dee51bc
Show file tree
Hide file tree
Showing 19 changed files with 167 additions and 56 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-ladybugs-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"livekit-client": patch
---

Replace internal instanceof checks with typeguards
18 changes: 9 additions & 9 deletions examples/demo/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
supportsAV1,
supportsVP9,
} from '../../src/index';
import { isSVCCodec } from '../../src/room/utils';
import { isLocalParticipant, isRemoteTrack, isSVCCodec } from '../../src/room/utils';

setLogLevel(LogLevel.debug);

Expand Down Expand Up @@ -165,7 +165,7 @@ const appActions = {
.on(RoomEvent.LocalTrackPublished, (pub) => {
const track = pub.track as LocalAudioTrack;

if (track instanceof LocalAudioTrack) {
if (isLocalAudioTrack(track)) {
const { calculateVolume } = createAudioAnalyser(track);

setInterval(() => {
Expand Down Expand Up @@ -495,7 +495,7 @@ window.appActions = appActions;

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

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

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

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

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

const micEnabled = micPub && micPub.isSubscribed && !micPub.isMuted;
if (micEnabled) {
if (!(participant instanceof LocalParticipant)) {
if (!isLocalParticipant(participant)) {
// don't attach local audio
audioELm.onloadeddata = () => {
if (participant.joinedAt && participant.joinedAt.getTime() < startTime) {
Expand Down Expand Up @@ -787,7 +787,7 @@ function renderBitrate() {
}

if (t.source === Track.Source.Camera) {
if (t.videoTrack instanceof RemoteVideoTrack) {
if (isRemoteTrack(t.videoTrack)) {
const codecElm = container.querySelector(`#codec-${p.identity}`)!;
codecElm.innerHTML = t.videoTrack.getDecoderImplementation() ?? '';
}
Expand Down
4 changes: 2 additions & 2 deletions src/e2ee/E2eeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import type Room from '../room/Room';
import { ConnectionState } from '../room/Room';
import { DeviceUnsupportedError } from '../room/errors';
import { EngineEvent, ParticipantEvent, RoomEvent } from '../room/events';
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 { isLocalTrack } from '../room/utils';
import type { BaseKeyProvider } from './KeyProvider';
import { E2EE_FLAG } from './constants';
import { type E2EEManagerCallbacks, EncryptionEvent, KeyProviderEvent } from './events';
Expand Down Expand Up @@ -317,7 +317,7 @@ export class E2EEManager
}

private setupE2EESender(track: Track, sender: RTCRtpSender) {
if (!(track instanceof LocalTrack) || !sender) {
if (!isLocalTrack(track) || !sender) {
if (!sender) log.warn('early return because sender is not ready');
return;
}
Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ import {
createAudioAnalyser,
getEmptyAudioStreamTrack,
getEmptyVideoStreamTrack,
isAudioTrack,
isBrowserSupported,
isLocalTrack,
isRemoteTrack,
isVideoTrack,
supportsAV1,
supportsAdaptiveStream,
supportsDynacast,
Expand Down Expand Up @@ -100,6 +104,10 @@ export {
supportsDynacast,
supportsVP9,
Mutex,
isAudioTrack,
isLocalTrack,
isRemoteTrack,
isVideoTrack,
};
export type {
AudioAnalyserOptions,
Expand Down
11 changes: 9 additions & 2 deletions src/room/RTCEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,14 @@ import type { Track } from './track/Track';
import type { TrackPublishOptions, VideoCodec } from './track/options';
import { getTrackPublicationInfo } from './track/utils';
import type { LoggerOptions } from './types';
import { isVideoCodec, isWeb, sleep, supportsAddTrack, supportsTransceiver } from './utils';
import {
isVideoCodec,
isVideoTrack,
isWeb,
sleep,
supportsAddTrack,
supportsTransceiver,
} from './utils';

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

if (track instanceof LocalVideoTrack) {
if (isVideoTrack(track)) {
track.codec = opts.videoCodec;
}

Expand Down
9 changes: 6 additions & 3 deletions src/room/Room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ import {
getEmptyAudioStreamTrack,
isBrowserSupported,
isCloud,
isLocalAudioTrack,
isLocalParticipant,
isReactNative,
isRemotePub,
isSafari,
isWeb,
supportsSetSinkId,
Expand Down Expand Up @@ -276,7 +279,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
this.e2eeManager.on(
EncryptionEvent.ParticipantEncryptionStatusChanged,
(enabled, participant) => {
if (participant instanceof LocalParticipant) {
if (isLocalParticipant(participant)) {
this.isE2EEEnabled = enabled;
}
this.emit(RoomEvent.ParticipantEncryptionStatusChanged, enabled, participant);
Expand Down Expand Up @@ -1950,7 +1953,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
private updateSubscriptions() {
for (const p of this.remoteParticipants.values()) {
for (const pub of p.videoTrackPublications.values()) {
if (pub.isSubscribed && pub instanceof RemoteTrackPublication) {
if (pub.isSubscribed && isRemotePub(pub)) {
pub.emitTrackUpdate();
}
}
Expand Down Expand Up @@ -2072,7 +2075,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)

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

if (pub.track instanceof LocalAudioTrack) {
if (isLocalAudioTrack(pub.track)) {
const trackIsSilent = await pub.track.checkForSilence();
if (trackIsSilent) {
this.emit(RoomEvent.LocalAudioSilenceDetected, pub);
Expand Down
43 changes: 23 additions & 20 deletions src/room/participant/LocalParticipant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,15 @@ import type { ChatMessage, DataPublishOptions } from '../types';
import {
Future,
compareVersions,
isAudioTrack,
isE2EESimulcastSupported,
isFireFox,
isLocalAudioTrack,
isLocalTrack,
isLocalVideoTrack,
isSVCCodec,
isSafari17,
isVideoTrack,
isWeb,
sleep,
supportsAV1,
Expand Down Expand Up @@ -640,9 +645,9 @@ export default class LocalParticipant extends Participant {
track.setAudioContext(this.audioContext);
}
track.mediaStream = stream;
if (track instanceof LocalAudioTrack && audioProcessor) {
if (isAudioTrack(track) && audioProcessor) {
await track.setProcessor(audioProcessor);
} else if (track instanceof LocalVideoTrack && videoProcessor) {
} else if (isVideoTrack(track) && videoProcessor) {
await track.setProcessor(videoProcessor);
}
return track;
Expand Down Expand Up @@ -717,15 +722,15 @@ export default class LocalParticipant extends Participant {
options?: TrackPublishOptions,
isRepublish = false,
): Promise<LocalTrackPublication> {
if (track instanceof LocalAudioTrack) {
if (isLocalAudioTrack(track)) {
track.setAudioContext(this.audioContext);
}

await this.reconnectFuture?.promise;
if (this.republishPromise && !isRepublish) {
await this.republishPromise;
}
if (track instanceof LocalTrack && this.pendingPublishPromises.has(track)) {
if (isLocalTrack(track) && this.pendingPublishPromises.has(track)) {
await this.pendingPublishPromises.get(track);
}
let defaultConstraints: MediaTrackConstraints | undefined;
Expand Down Expand Up @@ -857,15 +862,15 @@ export default class LocalParticipant extends Participant {

private async publish(track: LocalTrack, opts: TrackPublishOptions, isStereo: boolean) {
const existingTrackOfSource = Array.from(this.trackPublications.values()).find(
(publishedTrack) => track instanceof LocalTrack && publishedTrack.source === track.source,
(publishedTrack) => isLocalTrack(track) && publishedTrack.source === track.source,
);
if (existingTrackOfSource && track.source !== Track.Source.Unknown) {
this.log.info(`publishing a second track with the same source: ${track.source}`, {
...this.logContext,
...getLogContextFromTrack(track),
});
}
if (opts.stopMicTrackOnMute && track instanceof LocalAudioTrack) {
if (opts.stopMicTrackOnMute && isAudioTrack(track)) {
track.stopOnMute = true;
}

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

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

if (track instanceof LocalVideoTrack) {
if (isLocalVideoTrack(track)) {
opts.degradationPreference ??= getDefaultDegradationPreference(track);
track.setDegradationPreference(opts.degradationPreference);
}
Expand Down Expand Up @@ -1126,9 +1131,9 @@ export default class LocalParticipant extends Participant {
trackInfo: ti,
});

if (track instanceof LocalVideoTrack) {
if (isLocalVideoTrack(track)) {
track.startMonitor(this.engine.client);
} else if (track instanceof LocalAudioTrack) {
} else if (isLocalAudioTrack(track)) {
track.startMonitor();
}

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

if (!(track instanceof LocalVideoTrack)) {
if (!isLocalVideoTrack(track)) {
throw new TrackInvalidError('track is not a video track');
}

Expand Down Expand Up @@ -1236,7 +1241,7 @@ export default class LocalParticipant extends Participant {
track: LocalTrack | MediaStreamTrack,
stopOnUnpublish?: boolean,
): Promise<LocalTrackPublication | undefined> {
if (track instanceof LocalTrack) {
if (isLocalTrack(track)) {
const publishPromise = this.pendingPublishPromises.get(track);
if (publishPromise) {
this.log.info('awaiting publish promise before attempting to unpublish', {
Expand Down Expand Up @@ -1303,7 +1308,7 @@ export default class LocalParticipant extends Participant {
if (this.engine.removeTrack(trackSender)) {
negotiationNeeded = true;
}
if (track instanceof LocalVideoTrack) {
if (isLocalVideoTrack(track)) {
for (const [, trackInfo] of track.simulcastCodecs) {
if (trackInfo.sender) {
if (this.engine.removeTrack(trackInfo.sender)) {
Expand Down Expand Up @@ -1349,9 +1354,7 @@ export default class LocalParticipant extends Participant {
tracks: LocalTrack[] | MediaStreamTrack[],
): Promise<LocalTrackPublication[]> {
const results = await Promise.all(tracks.map((track) => this.unpublishTrack(track)));
return results.filter(
(track) => track instanceof LocalTrackPublication,
) as LocalTrackPublication[];
return results.filter((track) => !!track);
}

async republishAllTracks(options?: TrackPublishOptions, restartTracks: boolean = true) {
Expand Down Expand Up @@ -1379,7 +1382,7 @@ export default class LocalParticipant extends Participant {
!track.isMuted &&
track.source !== Track.Source.ScreenShare &&
track.source !== Track.Source.ScreenShareAudio &&
(track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) &&
(isLocalAudioTrack(track) || isLocalVideoTrack(track)) &&
!track.isUserProvided
) {
// generally we need to restart the track before publishing, often a full reconnect
Expand Down Expand Up @@ -1962,7 +1965,7 @@ export default class LocalParticipant extends Participant {
this.unpublishTrack(track);
} else if (track.isUserProvided) {
await track.mute();
} else if (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) {
} else if (isLocalAudioTrack(track) || isLocalVideoTrack(track)) {
try {
if (isWeb()) {
try {
Expand Down Expand Up @@ -1997,7 +2000,7 @@ export default class LocalParticipant extends Participant {
...this.logContext,
...getLogContextFromTrack(track),
});
if (track instanceof LocalAudioTrack) {
if (isLocalAudioTrack(track)) {
// fall back to default device if available
await track.restartTrack({ deviceId: 'default' });
} else {
Expand Down Expand Up @@ -2026,7 +2029,7 @@ export default class LocalParticipant extends Participant {

// this looks overly complicated due to this object tree
if (track instanceof MediaStreamTrack) {
if (localTrack instanceof LocalAudioTrack || localTrack instanceof LocalVideoTrack) {
if (isLocalAudioTrack(localTrack) || isLocalVideoTrack(localTrack)) {
if (localTrack.mediaStreamTrack === track) {
publication = <LocalTrackPublication>pub;
}
Expand Down
7 changes: 2 additions & 5 deletions src/room/participant/Participant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ import { EventEmitter } from 'events';
import type TypedEmitter from 'typed-emitter';
import log, { LoggerNames, type StructuredLogger, getLogger } from '../../logger';
import { ParticipantEvent, TrackEvent } from '../events';
import LocalAudioTrack from '../track/LocalAudioTrack';
import type LocalTrackPublication from '../track/LocalTrackPublication';
import RemoteAudioTrack from '../track/RemoteAudioTrack';
import type RemoteTrack from '../track/RemoteTrack';
import type RemoteTrackPublication from '../track/RemoteTrackPublication';
import { Track } from '../track/Track';
import type { TrackPublication } from '../track/TrackPublication';
import { diffAttributes } from '../track/utils';
import type { ChatMessage, LoggerOptions, TranscriptionSegment } from '../types';
import { isAudioTrack } from '../utils';

export enum ConnectionQuality {
Excellent = 'excellent',
Expand Down Expand Up @@ -317,9 +316,7 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
setAudioContext(ctx: AudioContext | undefined) {
this.audioContext = ctx;
this.audioTrackPublications.forEach(
(track) =>
(track.track instanceof RemoteAudioTrack || track.track instanceof LocalAudioTrack) &&
track.track.setAudioContext(ctx),
(track) => isAudioTrack(track.track) && track.track.setAudioContext(ctx),
);
}

Expand Down
Loading

0 comments on commit dee51bc

Please sign in to comment.