Skip to main content
Media tracks represent individual audio or video streams in a conference. lib-jitsi-meet provides three main track classes: JitsiTrack (base class), JitsiLocalTrack (local media), and JitsiRemoteTrack (remote participant media).

Track Hierarchy

All track classes inherit from the base JitsiTrack class:
JitsiTrack (base class)
├── JitsiLocalTrack (local audio/video)
└── JitsiRemoteTrack (remote participant audio/video)
From modules/RTC/JitsiTrack.ts:50:
export default class JitsiTrack extends Listenable {
    private audioLevel: number;
    private type: MediaType;
    stream: IExtendedMediaStream;
    track: IExtendedMediaStreamTrack;
    public conference: JitsiConference;
    public videoType: Optional<VideoType>;
    public disposed: boolean;
}

Track Types

Media Types

Tracks can be either audio or video:
enum MediaType {
    AUDIO = 'audio',
    VIDEO = 'video'
}

Video Types

Video tracks have additional type classification:
enum VideoType {
    CAMERA = 'camera',      // Camera/webcam video
    DESKTOP = 'desktop'     // Screen sharing
}

JitsiLocalTrack

JitsiLocalTrack represents media from the local participant (microphone, camera, or screen).

Creating Local Tracks

Use JitsiMeetJS.createLocalTracks() to create local tracks:
// Create audio and video tracks
const tracks = await JitsiMeetJS.createLocalTracks({
    devices: ['audio', 'video']
});

// Create audio only
const audioTracks = await JitsiMeetJS.createLocalTracks({
    devices: ['audio']
});

// Create video with specific resolution
const videoTracks = await JitsiMeetJS.createLocalTracks({
    devices: ['video'],
    resolution: 720,
    constraints: {
        video: {
            height: { ideal: 720 },
            width: { ideal: 1280 }
        }
    }
});

// Create desktop (screen sharing) track
const desktopTrack = await JitsiMeetJS.createLocalTracks({
    devices: ['desktop']
});

Track Constructor

From modules/RTC/JitsiLocalTrack.ts:127-147:
interface ITrackInfo {
    constraints: ITrackConstraints;      // Constraints used for track creation
    deviceId: string;                    // Device ID
    effects?: IStreamEffect[];           // Effects to apply
    facingMode?: CameraFacingMode;       // Camera facing mode (mobile)
    mediaType: MediaType;                // 'audio' or 'video'
    rtcId: number;                       // RTC module ID
    sourceId?: string;                   // Desktop sharing source ID
    sourceType?: string;                 // Source type
    stream: MediaStream;                 // WebRTC MediaStream
    track: MediaStreamTrack;             // WebRTC MediaStreamTrack
    videoType?: VideoType;               // Video type if applicable
}

constructor(trackInfo: ITrackInfo) {
    super(
        /* conference */ null,
        stream,
        track,
        /* streamInactiveHandler */ () => this.emit(JitsiTrackEvents.LOCAL_TRACK_STOPPED, this),
        mediaType,
        videoType
    );
}

Track Metadata

Local tracks maintain metadata:
interface ITrackMetadata {
    displaySurface?: string;  // For desktop tracks: 'monitor', 'window', 'application'
    timestamp: number;        // Track creation timestamp
}

const track = localTracks[0];
console.log('Track created at:', new Date(track.metadata.timestamp));
if (track.videoType === VideoType.DESKTOP) {
    console.log('Display surface:', track.metadata.displaySurface);
}

Muting and Unmuting

From modules/RTC/JitsiLocalTrack.ts:877-1051:
// Mute track
await localTrack.mute();

// Unmute track
await localTrack.unmute();

// Check mute status
const isMuted = localTrack.isMuted();

// Listen for mute changes
localTrack.on(JitsiTrackEvents.TRACK_MUTE_CHANGED, (track) => {
    console.log('Track muted:', track.isMuted());
});
Muting works differently for audio and video:
  • Audio tracks: track.enabled is set to false
  • Video tracks (browser-dependent): May remove the track from peer connection or just disable it
  • Desktop tracks: Often removed from connection when muted and recreated when unmuted

Attaching to DOM Elements

From modules/RTC/JitsiTrack.ts:266-277:
// Attach track to HTML element
const videoElement = document.getElementById('localVideo');
await localTrack.attach(videoElement);

// Detach from specific element
localTrack.detach(videoElement);

// Detach from all elements
localTrack.detach();
Attached containers are tracked in this.containers:
this.containers = [];  // Array of HTML elements displaying this track

Audio Levels

Monitor audio levels for local tracks:
localAudioTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, (audioLevel) => {
    // audioLevel is a value between 0 and 1
    console.log('Audio level:', audioLevel);
    updateAudioIndicator(audioLevel);
});
From modules/RTC/JitsiTrack.ts:499-527:
setAudioLevel(audioLevel: number, tpc?: TraceablePeerConnection): void {
    let newAudioLevel = audioLevel;

    // Reset to zero if muted
    if (browser.supportsReceiverStats() && typeof tpc !== 'undefined' && this.isMuted()) {
        newAudioLevel = 0;
    }

    if (this.audioLevel !== newAudioLevel) {
        this.audioLevel = newAudioLevel;
        this.emit(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, newAudioLevel, tpc);
    }
}

Stream Effects

Apply effects like background blur or virtual backgrounds:
// Define an effect
const blurEffect = {
    isEnabled: (track) => track.isVideoTrack(),
    startEffect: (stream) => {
        // Apply blur processing
        return processedStream;
    },
    stopEffect: () => {
        // Clean up effect
    }
};

// Apply effect to track
await localVideoTrack.setEffect(blurEffect);

// Remove effect
await localVideoTrack.setEffect(undefined);
From modules/RTC/JitsiLocalTrack.ts:927-989:
setEffect(effect?: IStreamEffect): Promise<void> {
    if (typeof this._streamEffect === 'undefined' && typeof effect === 'undefined') {
        return Promise.resolve();
    }

    if (typeof effect !== 'undefined' && !effect.isEnabled(this)) {
        return Promise.reject(new Error('Incompatible effect instance!'));
    }

    // Remove from conference, switch effect, add back to conference
    return conference._removeLocalTrackFromPc(this)
        .then(() => {
            this._switchStreamEffect(effect);
            return conference._addLocalTrackToPc(this);
        });
}

Device Management

Get device information:
// Get device ID
const deviceId = localTrack.getDeviceId();

// Get camera facing mode (mobile)
if (localTrack.isVideoTrack() && localTrack.videoType === VideoType.CAMERA) {
    const facingMode = localTrack.getCameraFacingMode();
    console.log('Camera facing:', facingMode); // 'user' or 'environment'
}

// Get track resolution
if (localTrack.isVideoTrack()) {
    const resolution = localTrack.getCaptureResolution();
    console.log('Resolution:', resolution);
}

Track Lifecycle

From modules/RTC/JitsiLocalTrack.ts:665-692:
// Dispose track when done
await localTrack.dispose();

// Check if track is disposed
if (localTrack.disposed) {
    console.log('Track is disposed');
}

// Check if track has ended
if (localTrack.isEnded()) {
    console.log('Track ended (device disconnected or stopped)');
}

// Listen for track stopped event
localTrack.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, () => {
    console.log('Track stopped');
});
Always dispose of tracks when finished to free up media resources and stop camera/microphone access.

Source Name and SSRC

Tracks are identified by source name and SSRC:
// Get source name (used for signaling)
const sourceName = localTrack.getSourceName();

// Get SSRC (Synchronization Source identifier)
const ssrc = localTrack.getSsrc();

// Get participant ID
const participantId = localTrack.getParticipantId();

JitsiRemoteTrack

JitsiRemoteTrack represents media from remote participants.

Constructor

From modules/RTC/JitsiRemoteTrack.ts:74-134:
constructor(
    rtc: RTC,
    conference: JitsiConference,
    ownerEndpointId: string,        // Participant ID
    stream: MediaStream,
    track: MediaStreamTrack,
    mediaType: MediaType,
    videoType: VideoType,
    ssrc: number,                    // Must be a number
    muted: boolean,                  // Initial mute state
    isP2P: boolean,                  // P2P or JVB connection
    sourceName: string               // Source name for track
) {
    super(conference, stream, track, () => {}, mediaType, videoType);
    
    // Validate SSRC is a number
    if (typeof ssrc !== 'number') {
        throw new TypeError(`SSRC ${ssrc} is not a number`);
    }
    
    this._ssrc = ssrc;
    this.ownerEndpointId = ownerEndpointId;
    this._muted = muted;
    this.isP2P = isP2P;
    this._sourceName = sourceName;
}

Receiving Remote Tracks

Remote tracks are received through conference events:
conference.on(JitsiConferenceEvents.TRACK_ADDED, (track) => {
    if (track.isLocal()) {
        return; // Skip local tracks
    }
    
    const participantId = track.getParticipantId();
    const mediaType = track.getType();
    
    if (track.isVideoTrack()) {
        const videoElement = document.getElementById(`video-${participantId}`);
        track.attach(videoElement);
    } else if (track.isAudioTrack()) {
        const audioElement = document.getElementById(`audio-${participantId}`);
        track.attach(audioElement);
    }
});

conference.on(JitsiConferenceEvents.TRACK_REMOVED, (track) => {
    track.detach();
    track.dispose();
});

Remote Track Properties

// Get participant ID
const participantId = remoteTrack.getParticipantId();

// Get source name
const sourceName = remoteTrack.getSourceName();

// Get SSRC
const ssrc = remoteTrack.getSsrc();

// Check if P2P track
const isP2P = remoteTrack.isP2P;

// Check mute state
const isMuted = remoteTrack.isMuted();

// Get video type
if (remoteTrack.isVideoTrack()) {
    const videoType = remoteTrack.getVideoType(); // 'camera' or 'desktop'
}

Mute State

Remote track mute state is controlled by the remote participant:
remoteTrack.on(JitsiTrackEvents.TRACK_MUTE_CHANGED, (track) => {
    console.log('Remote track muted:', track.isMuted());
    
    if (track.isMuted()) {
        // Show muted indicator
    } else {
        // Hide muted indicator
    }
});
From modules/RTC/JitsiRemoteTrack.ts:446-464:
setMute(value: boolean): void {
    if (this._muted === value) {
        return;
    }

    if (value) {
        this._hasBeenMuted = true;
    }

    if (this.stream) {
        this.stream.muted = value;
    }

    this._muted = value;
    logger.info(`Mute ${this}: ${value}`);
    this.emit(JitsiTrackEvents.TRACK_MUTE_CHANGED, this);
}

Track Streaming Status

Monitor if a remote track is actively receiving data:
enum TrackStreamingStatus {
    ACTIVE = 'active',           // Receiving data
    INACTIVE = 'inactive',       // Not receiving data
    INTERRUPTED = 'interrupted', // Connection interrupted
    RESTORING = 'restoring'      // Attempting to restore
}

remoteTrack.on(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED, 
    (track, status) => {
        console.log('Streaming status:', status);
        
        switch(status) {
            case TrackStreamingStatus.ACTIVE:
                // Show video
                break;
            case TrackStreamingStatus.INACTIVE:
                // Show avatar/placeholder
                break;
            case TrackStreamingStatus.INTERRUPTED:
                // Show connection issues
                break;
        }
    }
);

// Get current status
const status = remoteTrack.getTrackStreamingStatus();

Video Type Changes

Detect when a participant switches between camera and screen sharing:
remoteTrack.on(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED, (type) => {
    console.log('Video type changed to:', type);
    
    if (type === VideoType.DESKTOP) {
        // Handle screen sharing
        expandVideoToFullScreen();
    } else {
        // Handle camera video
        showInNormalView();
    }
});
From modules/RTC/JitsiRemoteTrack.ts:380-386:
_setVideoType(type: VideoType): void {
    if (this.videoType === type) {
        return;
    }
    this.videoType = type;
    this.emit(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED, type);
}

Common Track Operations

Track Information

Both local and remote tracks provide:
// Basic info
const trackId = track.getId();              // Stream ID
const trackLabel = track.getTrackLabel();   // MediaStreamTrack label
const mediaType = track.getType();          // 'audio' or 'video'

// Check track type
const isAudio = track.isAudioTrack();
const isVideo = track.isVideoTrack();
const isLocal = track.isLocal();

// Get underlying WebRTC objects
const mediaStream = track.getOriginalStream();
const mediaStreamTrack = track.getTrack();

Audio Output Device

Change the audio output device for playback:
// Set audio output device (for audio tracks only)
await track.setAudioOutput(deviceId);

track.on(JitsiTrackEvents.TRACK_AUDIO_OUTPUT_CHANGED, (deviceId) => {
    console.log('Audio output changed to:', deviceId);
});
From modules/RTC/JitsiTrack.ts:536-568:
setAudioOutput(audioOutputDeviceId: string): Promise<void> {
    if (!RTCUtils.isDeviceChangeAvailable('output')) {
        return Promise.reject(
            new Error('Audio output device change is not supported'));
    }

    if (this.isVideoTrack()) {
        return Promise.resolve();
    }

    return Promise.all(
        this.containers.map(element =>
            (element as HTMLAudioElement | HTMLVideoElement)
                .setSinkId(audioOutputDeviceId)
        )
    ).then(() => {
        this.emit(JitsiTrackEvents.TRACK_AUDIO_OUTPUT_CHANGED, audioOutputDeviceId);
    });
}

Track Dimensions

For video tracks:
if (track.isVideoTrack()) {
    const height = track.getHeight();  // Normalized landscape height
    const width = track.getWidth();    // Normalized landscape width
    
    console.log(`Video dimensions: ${width}x${height}`);
}

Track Events

All track events from JitsiTrackEvents:
EventFired OnDescription
LOCAL_TRACK_STOPPEDLocalLocal track stopped
TRACK_AUDIO_LEVEL_CHANGEDBothAudio level changed
TRACK_AUDIO_OUTPUT_CHANGEDBothAudio output device changed
TRACK_MUTE_CHANGEDBothMute state changed
TRACK_VIDEOTYPE_CHANGEDRemoteVideo type changed
TRACK_STREAMING_STATUS_CHANGEDRemoteStreaming status changed
NO_AUDIO_INPUTLocalNo audio input detected
NO_DATA_FROM_SOURCELocalNo data from source

Best Practices

Always dispose of tracks to free resources:
// Create tracks
const tracks = await JitsiMeetJS.createLocalTracks({
    devices: ['audio', 'video']
});

// Use tracks...

// Dispose when done
for (const track of tracks) {
    if (conference) {
        await conference.removeTrack(track);
    }
    await track.dispose();
}
Gracefully handle track creation failures:
try {
    const tracks = await JitsiMeetJS.createLocalTracks({
        devices: ['audio', 'video']
    });
} catch (error) {
    console.error('Failed to create tracks:', error);
    
    // Try creating just audio
    try {
        const audioTracks = await JitsiMeetJS.createLocalTracks({
            devices: ['audio']
        });
    } catch (audioError) {
        // Handle complete failure
    }
}
Always detach tracks before disposal:
// When participant leaves
conference.on(JitsiConferenceEvents.USER_LEFT, (id, participant) => {
    const tracks = participant.getTracks();
    tracks.forEach(track => {
        track.detach();  // Detach from all containers
        track.dispose(); // Clean up resources
    });
});
Monitor track streaming status for remote tracks:
remoteTrack.on(
    JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
    (track, status) => {
        if (status === TrackStreamingStatus.INTERRUPTED) {
            showConnectionWarning(track.getParticipantId());
        } else if (status === TrackStreamingStatus.ACTIVE) {
            hideConnectionWarning(track.getParticipantId());
        }
    }
);

JitsiConference

Learn about adding and removing tracks from conferences

JitsiParticipant

Understand participant track management

Build docs developers (and LLMs) love