diff --git a/.changeset/fair-ladybugs-divide.md b/.changeset/fair-ladybugs-divide.md new file mode 100644 index 0000000000..69fe112a31 --- /dev/null +++ b/.changeset/fair-ladybugs-divide.md @@ -0,0 +1,5 @@ +--- +"livekit-client": patch +--- + +Replace internal instanceof checks with typeguards diff --git a/examples/demo/demo.ts b/examples/demo/demo.ts index 6078bbbb27..e5497cbc2a 100644 --- a/examples/demo/demo.ts +++ b/examples/demo/demo.ts @@ -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); @@ -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(() => { @@ -495,7 +495,7 @@ window.appActions = appActions; function handleChatMessage(msg: ChatMessage, participant?: LocalParticipant | RemoteParticipant) { ($('chat')).value += - `${participant?.identity}${participant instanceof LocalParticipant ? ' (me)' : ''}: ${msg.message}\n`; + `${participant?.identity}${isLocalParticipant(participant) ? ' (me)' : ''}: ${msg.message}\n`; } function participantConnected(participant: Participant) { @@ -596,7 +596,7 @@ function renderParticipant(participant: Participant, remove: boolean = false) { ${ - participant instanceof RemoteParticipant + !isLocalParticipant(participant) ? `
` @@ -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}`)!; @@ -642,7 +642,7 @@ function renderParticipant(participant: Participant, remove: boolean = false) { div!.classList.remove('speaking'); } - if (participant instanceof RemoteParticipant) { + if (!isLocalParticipant(participant)) { const volumeSlider = container.querySelector(`#volume-${identity}`); volumeSlider.addEventListener('input', (ev) => { participant.setVolume(Number.parseFloat((ev.target as HTMLInputElement).value)); @@ -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)) { @@ -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) { @@ -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() ?? ''; } diff --git a/src/e2ee/E2eeManager.ts b/src/e2ee/E2eeManager.ts index 6597972038..8d1cf509ef 100644 --- a/src/e2ee/E2eeManager.ts +++ b/src/e2ee/E2eeManager.ts @@ -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'; @@ -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; } diff --git a/src/index.ts b/src/index.ts index aed9cb29cf..9a2ee0a8b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,11 @@ import { createAudioAnalyser, getEmptyAudioStreamTrack, getEmptyVideoStreamTrack, + isAudioTrack, isBrowserSupported, + isLocalTrack, + isRemoteTrack, + isVideoTrack, supportsAV1, supportsAdaptiveStream, supportsDynacast, @@ -100,6 +104,10 @@ export { supportsDynacast, supportsVP9, Mutex, + isAudioTrack, + isLocalTrack, + isRemoteTrack, + isVideoTrack, }; export type { AudioAnalyserOptions, diff --git a/src/room/RTCEngine.ts b/src/room/RTCEngine.ts index 41a3aca26c..72d322afc3 100644 --- a/src/room/RTCEngine.ts +++ b/src/room/RTCEngine.ts @@ -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'; @@ -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; } diff --git a/src/room/Room.ts b/src/room/Room.ts index f9fdd0d083..8e544edb26 100644 --- a/src/room/Room.ts +++ b/src/room/Room.ts @@ -85,7 +85,10 @@ import { getEmptyAudioStreamTrack, isBrowserSupported, isCloud, + isLocalAudioTrack, + isLocalParticipant, isReactNative, + isRemotePub, isSafari, isWeb, supportsSetSinkId, @@ -276,7 +279,7 @@ class Room extends (EventEmitter as new () => TypedEmitter) this.e2eeManager.on( EncryptionEvent.ParticipantEncryptionStatusChanged, (enabled, participant) => { - if (participant instanceof LocalParticipant) { + if (isLocalParticipant(participant)) { this.isE2EEEnabled = enabled; } this.emit(RoomEvent.ParticipantEncryptionStatusChanged, enabled, participant); @@ -1950,7 +1953,7 @@ class Room extends (EventEmitter as new () => TypedEmitter) 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(); } } @@ -2072,7 +2075,7 @@ class Room extends (EventEmitter as new () => TypedEmitter) 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); diff --git a/src/room/participant/LocalParticipant.ts b/src/room/participant/LocalParticipant.ts index b19ecfd234..05417cd3d6 100644 --- a/src/room/participant/LocalParticipant.ts +++ b/src/room/participant/LocalParticipant.ts @@ -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, @@ -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; @@ -717,7 +722,7 @@ export default class LocalParticipant extends Participant { options?: TrackPublishOptions, isRepublish = false, ): Promise { - if (track instanceof LocalAudioTrack) { + if (isLocalAudioTrack(track)) { track.setAudioContext(this.audioContext); } @@ -725,7 +730,7 @@ export default class LocalParticipant extends Participant { 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; @@ -857,7 +862,7 @@ 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}`, { @@ -865,7 +870,7 @@ export default class LocalParticipant extends Participant { ...getLogContextFromTrack(track), }); } - if (opts.stopMicTrackOnMute && track instanceof LocalAudioTrack) { + if (opts.stopMicTrackOnMute && isAudioTrack(track)) { track.stopOnMute = true; } @@ -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 @@ -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); } @@ -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(); } @@ -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'); } @@ -1236,7 +1241,7 @@ export default class LocalParticipant extends Participant { track: LocalTrack | MediaStreamTrack, stopOnUnpublish?: boolean, ): Promise { - 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', { @@ -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)) { @@ -1349,9 +1354,7 @@ export default class LocalParticipant extends Participant { tracks: LocalTrack[] | MediaStreamTrack[], ): Promise { 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) { @@ -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 @@ -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 { @@ -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 { @@ -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 = pub; } diff --git a/src/room/participant/Participant.ts b/src/room/participant/Participant.ts index d80f29482d..b0f7794305 100644 --- a/src/room/participant/Participant.ts +++ b/src/room/participant/Participant.ts @@ -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', @@ -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), ); } diff --git a/src/room/participant/RemoteParticipant.ts b/src/room/participant/RemoteParticipant.ts index ccd17584e7..cea7e49f18 100644 --- a/src/room/participant/RemoteParticipant.ts +++ b/src/room/participant/RemoteParticipant.ts @@ -16,6 +16,7 @@ import type { AudioOutputOptions } from '../track/options'; import type { AdaptiveStreamSettings } from '../track/types'; import { getLogContextFromTrack } from '../track/utils'; import type { LoggerOptions } from '../types'; +import { isAudioTrack, isRemoteTrack } from '../utils'; import Participant, { ParticipantKind } from './Participant'; import type { ParticipantEventCallbacks } from './Participant'; @@ -239,7 +240,7 @@ export default class RemoteParticipant extends Participant { publication.setTrack(track); // set participant volumes on new audio tracks - if (this.volumeMap.has(publication.source) && track instanceof RemoteAudioTrack) { + if (this.volumeMap.has(publication.source) && isRemoteTrack(track) && isAudioTrack(track)) { track.setVolume(this.volumeMap.get(publication.source)!); } @@ -367,7 +368,7 @@ export default class RemoteParticipant extends Participant { this.audioOutput = output; const promises: Promise[] = []; this.audioTrackPublications.forEach((pub) => { - if (pub.track instanceof RemoteAudioTrack) { + if (isAudioTrack(pub.track) && isRemoteTrack(pub.track)) { promises.push(pub.track.setSinkId(output.deviceId ?? 'default')); } }); diff --git a/src/room/track/LocalTrack.ts b/src/room/track/LocalTrack.ts index a736389958..3f09d0473a 100644 --- a/src/room/track/LocalTrack.ts +++ b/src/room/track/LocalTrack.ts @@ -120,6 +120,10 @@ export default abstract class LocalTrack< return this.processor?.processedTrack ?? this._mediaStreamTrack; } + get isLocal() { + return true; + } + /** * @internal * returns mediaStreamTrack settings of the capturing mediastreamtrack source - ignoring processors diff --git a/src/room/track/LocalTrackPublication.ts b/src/room/track/LocalTrackPublication.ts index 6af5b9d80f..2cc859bb6c 100644 --- a/src/room/track/LocalTrackPublication.ts +++ b/src/room/track/LocalTrackPublication.ts @@ -1,6 +1,7 @@ import { AudioTrackFeature, TrackInfo } from '@livekit/protocol'; import { TrackEvent } from '../events'; import type { LoggerOptions } from '../types'; +import { isAudioTrack } from '../utils'; import LocalAudioTrack from './LocalAudioTrack'; import type LocalTrack from './LocalTrack'; import type LocalVideoTrack from './LocalVideoTrack'; @@ -51,6 +52,10 @@ export default class LocalTrackPublication extends TrackPublication { return super.videoTrack as LocalVideoTrack | undefined; } + get isLocal() { + return true; + } + /** * Mute the track associated with this publication */ @@ -83,7 +88,7 @@ export default class LocalTrackPublication extends TrackPublication { } getTrackFeatures() { - if (this.track instanceof LocalAudioTrack) { + if (isAudioTrack(this.track)) { const settings = this.track!.getSourceTrackSettings(); const features: Set = new Set(); if (settings.autoGainControl) { diff --git a/src/room/track/RemoteTrack.ts b/src/room/track/RemoteTrack.ts index 0504a72619..3c378aa1cb 100644 --- a/src/room/track/RemoteTrack.ts +++ b/src/room/track/RemoteTrack.ts @@ -23,6 +23,10 @@ export default abstract class RemoteTrack< this.receiver = receiver; } + get isLocal() { + return false; + } + /** @internal */ setMuted(muted: boolean) { if (this.isMuted !== muted) { diff --git a/src/room/track/RemoteTrackPublication.ts b/src/room/track/RemoteTrackPublication.ts index 6163dba23e..80d4fff1b4 100644 --- a/src/room/track/RemoteTrackPublication.ts +++ b/src/room/track/RemoteTrackPublication.ts @@ -7,8 +7,8 @@ import { } from '@livekit/protocol'; import { TrackEvent } from '../events'; import type { LoggerOptions } from '../types'; +import { isRemoteVideoTrack } from '../utils'; import type RemoteTrack from './RemoteTrack'; -import RemoteVideoTrack from './RemoteVideoTrack'; import { Track, VideoQuality } from './Track'; import { TrackPublication } from './TrackPublication'; @@ -108,6 +108,10 @@ export default class RemoteTrackPublication extends TrackPublication { return !this.disabled; } + get isLocal() { + return false; + } + /** * disable server from sending down data for this track. this is useful when * the participant is off screen, you may disable streaming down their video @@ -150,7 +154,7 @@ export default class RemoteTrackPublication extends TrackPublication { ) { return; } - if (this.track instanceof RemoteVideoTrack) { + if (isRemoteVideoTrack(this.track)) { this.videoDimensions = dimensions; } this.currentVideoQuality = undefined; @@ -163,7 +167,7 @@ export default class RemoteTrackPublication extends TrackPublication { return; } - if (!(this.track instanceof RemoteVideoTrack)) { + if (!isRemoteVideoTrack(this.track)) { return; } @@ -276,7 +280,7 @@ export default class RemoteTrackPublication extends TrackPublication { }; protected get isAdaptiveStream(): boolean { - return this.track instanceof RemoteVideoTrack && this.track.isAdaptiveStream; + return isRemoteVideoTrack(this.track) && this.track.isAdaptiveStream; } protected handleVisibilityChange = (visible: boolean) => { diff --git a/src/room/track/Track.ts b/src/room/track/Track.ts index e690762adb..718badbc30 100644 --- a/src/room/track/Track.ts +++ b/src/room/track/Track.ts @@ -106,6 +106,8 @@ export abstract class Track< return this._mediaStreamTrack; } + abstract get isLocal(): boolean; + /** * @internal * used for keep mediaStream's first id, since it's id might change diff --git a/src/room/track/TrackPublication.ts b/src/room/track/TrackPublication.ts index ca48f3310e..4f2e123c2a 100644 --- a/src/room/track/TrackPublication.ts +++ b/src/room/track/TrackPublication.ts @@ -10,6 +10,7 @@ import type TypedEventEmitter from 'typed-emitter'; import log, { LoggerNames, getLogger } from '../../logger'; import { TrackEvent } from '../events'; import type { LoggerOptions, TranscriptionSegment } from '../types'; +import { isAudioTrack, isVideoTrack } from '../utils'; import LocalAudioTrack from './LocalAudioTrack'; import LocalVideoTrack from './LocalVideoTrack'; import RemoteAudioTrack from './RemoteAudioTrack'; @@ -18,7 +19,7 @@ import RemoteVideoTrack from './RemoteVideoTrack'; import { Track } from './Track'; import { getLogContextFromTrack } from './utils'; -export class TrackPublication extends (EventEmitter as new () => TypedEventEmitter) { +export abstract class TrackPublication extends (EventEmitter as new () => TypedEventEmitter) { kind: Track.Kind; trackName: string; @@ -99,11 +100,13 @@ export class TrackPublication extends (EventEmitter as new () => TypedEventEmitt return this.encryption !== Encryption_Type.NONE; } + abstract get isLocal(): boolean; + /** * an [AudioTrack] if this publication holds an audio track */ get audioTrack(): LocalAudioTrack | RemoteAudioTrack | undefined { - if (this.track instanceof LocalAudioTrack || this.track instanceof RemoteAudioTrack) { + if (isAudioTrack(this.track)) { return this.track; } } @@ -112,7 +115,7 @@ export class TrackPublication extends (EventEmitter as new () => TypedEventEmitt * an [VideoTrack] if this publication holds a video track */ get videoTrack(): LocalVideoTrack | RemoteVideoTrack | undefined { - if (this.track instanceof LocalVideoTrack || this.track instanceof RemoteVideoTrack) { + if (isVideoTrack(this.track)) { return this.track; } } diff --git a/src/room/track/create.ts b/src/room/track/create.ts index 95cff22ace..a3f47bfa7e 100644 --- a/src/room/track/create.ts +++ b/src/room/track/create.ts @@ -2,7 +2,7 @@ import DeviceManager from '../DeviceManager'; import { audioDefaults, videoDefaults } from '../defaults'; import { DeviceUnsupportedError, TrackInvalidError } from '../errors'; import { mediaTrackToLocalTrack } from '../participant/publishUtils'; -import { isSafari17 } from '../utils'; +import { isAudioTrack, isSafari17, isVideoTrack } from '../utils'; import LocalAudioTrack from './LocalAudioTrack'; import type LocalTrack from './LocalTrack'; import LocalVideoTrack from './LocalVideoTrack'; @@ -81,9 +81,10 @@ export async function createLocalTracks( track.source = Track.Source.Microphone; } 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); } diff --git a/src/room/track/facingMode.ts b/src/room/track/facingMode.ts index eb54e81c66..8003e71ad5 100644 --- a/src/room/track/facingMode.ts +++ b/src/room/track/facingMode.ts @@ -1,4 +1,5 @@ import log from '../../logger'; +import { isLocalTrack } from '../utils'; import LocalTrack from './LocalTrack'; import type { VideoCaptureOptions } from './options'; @@ -37,7 +38,7 @@ export function facingModeFromLocalTrack( localTrack: LocalTrack | MediaStreamTrack, options: FacingModeFromLocalTrackOptions = {}, ): FacingModeFromLocalTrackReturnValue { - const track = localTrack instanceof LocalTrack ? localTrack.mediaStreamTrack : localTrack; + const track = isLocalTrack(localTrack) ? localTrack.mediaStreamTrack : localTrack; const trackSettings = track.getSettings(); let result: FacingModeFromLocalTrackReturnValue = { facingMode: options.defaultFacingMode ?? 'user', diff --git a/src/room/track/utils.ts b/src/room/track/utils.ts index ca7ae6324d..88f6e5d488 100644 --- a/src/room/track/utils.ts +++ b/src/room/track/utils.ts @@ -222,7 +222,7 @@ export function getTrackPublicationInfo( } export function getLogContextFromTrack(track: Track | TrackPublication): Record { - if (track instanceof Track) { + if ('mediaStreamTrack' in track) { return { trackID: track.sid, source: track.source, diff --git a/src/room/utils.ts b/src/room/utils.ts index 3b8c427310..de2c252bd0 100644 --- a/src/room/utils.ts +++ b/src/room/utils.ts @@ -8,9 +8,20 @@ import { import { getBrowser } from '../utils/browserParser'; import { protocolVersion, version } from '../version'; import { type ConnectionError, ConnectionErrorReason } from './errors'; +import type LocalParticipant from './participant/LocalParticipant'; +import type Participant from './participant/Participant'; +import type RemoteParticipant from './participant/RemoteParticipant'; import CriticalTimers from './timers'; import type LocalAudioTrack from './track/LocalAudioTrack'; +import type LocalTrack from './track/LocalTrack'; +import type LocalTrackPublication from './track/LocalTrackPublication'; +import type LocalVideoTrack from './track/LocalVideoTrack'; import type RemoteAudioTrack from './track/RemoteAudioTrack'; +import type RemoteTrack from './track/RemoteTrack'; +import type RemoteTrackPublication from './track/RemoteTrackPublication'; +import type RemoteVideoTrack from './track/RemoteVideoTrack'; +import { Track } from './track/Track'; +import type { TrackPublication } from './track/TrackPublication'; import { type VideoCodec, videoCodecs } from './track/options'; import { getNewAudioContext } from './track/utils'; import type { ChatMessage, LiveKitReactNativeInfo, TranscriptionSegment } from './types'; @@ -548,3 +559,55 @@ export function getDisconnectReasonFromConnectionError(e: ConnectionError) { return DisconnectReason.UNKNOWN_REASON; } } + +export function isLocalTrack(track: Track | MediaStreamTrack | undefined): track is LocalTrack { + return !!track && !(track instanceof MediaStreamTrack) && track.isLocal; +} + +export function isAudioTrack( + track: Track | undefined, +): track is LocalAudioTrack | RemoteAudioTrack { + return !!track && track.kind == Track.Kind.Audio; +} + +export function isVideoTrack( + track: Track | undefined, +): track is LocalVideoTrack | RemoteVideoTrack { + return !!track && track.kind == Track.Kind.Video; +} + +export function isLocalVideoTrack( + track: Track | MediaStreamTrack | undefined, +): track is LocalVideoTrack { + return isLocalTrack(track) && isVideoTrack(track); +} + +export function isLocalAudioTrack( + track: Track | MediaStreamTrack | undefined, +): track is LocalAudioTrack { + return isLocalTrack(track) && isAudioTrack(track); +} + +export function isRemoteTrack(track: Track | undefined): track is RemoteTrack { + return !!track && !track.isLocal; +} + +export function isRemotePub(pub: TrackPublication | undefined): pub is RemoteTrackPublication { + return !!pub && !pub.isLocal; +} + +export function isLocalPub(pub: TrackPublication | undefined): pub is LocalTrackPublication { + return !!pub && !pub.isLocal; +} + +export function isRemoteVideoTrack(track: Track | undefined): track is RemoteVideoTrack { + return isRemoteTrack(track) && isVideoTrack(track); +} + +export function isLocalParticipant(p: Participant): p is LocalParticipant { + return p.isLocal; +} + +export function isRemoteParticipant(p: Participant): p is RemoteParticipant { + return !p.isLocal; +}