jitsi/jitsi-meet · error · Error

NoLocalStreams

Error message

NoLocalStreams

What it means

Thrown by startLocalRecording on web when the user has no live local audio or video track to record. Before starting a local recording, the manager checks the local tracks from redux; video tracks that are not in readyState 'live' are discarded. If both audio and video are absent (or ended), recording cannot proceed and 'NoLocalStreams' is raised.

Source

Thrown at react/features/recording/components/Recording/LocalRecordingManager.web.ts:177

        this.writableStream = await this.fileHandle?.createWritable();

        const supportsCaptureHandle = !isEmbedded();
        const tabId = uuidV4();

        this.selfRecording.on = onlySelf;
        let gdmStream: MediaStream = new MediaStream();
        const tracks = getTrackState(getState());

        if (onlySelf) {
            const audioTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
            let videoTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.VIDEO)?.jitsiTrack?.track;

            if (videoTrack && videoTrack.readyState !== 'live') {
                videoTrack = undefined;
            }

            if (!audioTrack && !videoTrack) {
                throw new Error('NoLocalStreams');
            }

            this.selfRecording.withVideo = Boolean(videoTrack);
            const localTracks: MediaStreamTrack[] = [];

            audioTrack && localTracks.push(audioTrack.clone());
            videoTrack && localTracks.push(videoTrack.clone());
            this.stream = new MediaStream(localTracks);
        } else {
            if (supportsCaptureHandle) {
                // @ts-ignore
                navigator.mediaDevices.setCaptureHandleConfig({
                    handle: `JitsiMeet-${tabId}`,
                    permittedOrigins: [ '*' ]
                });
            }

            gdmStream = await navigator.mediaDevices.getDisplayMedia({

View on GitHub (pinned to 98de6219cc)

Solutions

  1. Verify the participant has at least one live local track (unmuted audio or active video) before showing/enabling the local recording button
  2. Check track.readyState === 'live' on the raw jitsiTrack.track before calling startLocalRecording
  3. If permissions were denied, re-request mic/camera access and retry once tracks exist
  4. Handle the thrown error in the caller to show a 'you need at least one active mic or camera' notification instead of crashing

Example fix

// before
await localRecordingManager.startLocalRecording();

// after
const tracks = getState()['features/base/tracks'];
const audio = getLocalTrack(tracks, MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
const video = getLocalTrack(tracks, MEDIA_TYPE.VIDEO)?.jitsiTrack?.track;
const hasLive = t => t && t.readyState === 'live';
if (!hasLive(audio) && !hasLive(video)) {
    dispatch(showErrorNotification({ titleKey: 'localRecording.noStreams' }));
} else {
    await localRecordingManager.startLocalRecording();
}
Defensive patterns

Strategy: validation

Validate before calling

const tracks = getState()['features/base/tracks'];
const audio = getLocalTrack(tracks, MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
const video = getLocalTrack(tracks, MEDIA_TYPE.VIDEO)?.jitsiTrack?.track;
const hasLiveTrack = (audio && audio.readyState === 'live')
    || (video && video.readyState === 'live');
if (!hasLiveTrack) {
    // show 'enable mic or camera first' notification instead of calling
}

Type guard

const isLiveTrack = (t?: MediaStreamTrack | null): t is MediaStreamTrack =>
    Boolean(t) && t!.readyState === 'live';

Try / catch

try {
    await manager.startLocalRecording();
} catch (e: unknown) {
    if (e instanceof Error && e.message === 'NoLocalStreams') {
        showError('Enable your microphone or camera before recording.');
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Calling startLocalRecording() when the local participant's audio is muted-without-track (or track ended) and video is disabled or its MediaStreamTrack.readyState !== 'live'. This includes calling before media permission was granted, after stopping all tracks, or when the user joined in audio-only/muted mode with no gdm audio source.

Common situations: User joins with mic/camera permissions denied or revoked mid-call; track was stopped and disposed; startLocalRecording invoked from UI before waitForInitialization/local tracks are ready; browser blocked getUserMedia.

Related errors


AI-assisted analysis of jitsi/jitsi-meet@98de6219cc (2026-08-28). Data as JSON: /api/errors/781344ed67d6a172. Report an issue: GitHub.