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.

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.

Appendix: 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)