fresh-television-67623
02/18/2026, 4:10 AMtall-airline-16756
02/18/2026, 4:11 AMtall-airline-16756
02/18/2026, 4:11 AMreplaceTrack() so participants don’t see a prolonged black screen.
Why this works
• When a camera is disconnected the browser’s MediaStreamTrack will fire ended (or the device list will change). If you keep the old (now-ended) track published, the remote view becomes black. Replacing the track with a fresh LocalVideoTrack from another device resumes video while keeping the published state.
Suggested implementation (JavaScript / LiveKit client)
1. Listen for the local video track’s underlying MediaStreamTrack ending.
2. If ended, get the list of videoinput devices, pick one, create a new camera track, then call replaceTrack().
Example (adapt to your app structure):
import { RoomEvent, Track, LocalVideoTrack } from 'livekit-client';
// assumes `room` is your connected Room and you have access to the localParticipant
// helper: get devices
async function getCameras() {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(d => d.kind === 'videoinput');
}
// attach handler when you initially create/publish the track
function monitorLocalVideoTrack(localTrack) {
const mediaTracks = localTrack.mediaStreamTrack.getVideoTracks();
if (!mediaTracks || mediaTracks.length === 0) return;
mediaTracks.forEach(vt => {
vt.onended = async () => {
console.log('Local camera ended/disconnected — attempting to switch cameras');
const cams = await getCameras();
// remove the device that ended if it still appears; choose another
if (cams.length === 0) {
console.warn('No cameras available to switch to');
return;
}
// Pick the first available camera that is not the one that ended
const newDevice = cams[0];
try {
const newTrack = await LocalVideoTrack.createCameraTrack({
deviceId: { exact: newDevice.deviceId },
});
// Replace currently published track with the new one
// If you have the Publication object or the original LocalTrack reference, pass that. Example uses room.localParticipant
await room.localParticipant.replaceTrack(localTrack.track, newTrack);
// Start monitoring the new track too
monitorLocalVideoTrack(newTrack);
console.log('Switched to camera:', newDevice.label || newDevice.deviceId);
} catch (err) {
console.error('Failed to create/replace camera track', err);
}
};
});
}
Notes and tips
• Use navigator.mediaDevices.enumerateDevices() to see available cameras and their deviceIds.
• If you previously created the LocalVideoTrack with a particular deviceId constraint, use that logic to avoid reselecting the same (disconnected) device.
• Call monitorLocalVideoTrack() whenever you create or replace the local video track so future disconnects are handled.
• If you want proactive switching (before track ended), also listen to navigator.mediaDevices.ondevicechange and check enumerateDevices() to see if the selected device disappeared.
• Keep user UX in mind: consider showing a small notification that the camera changed or prompt the user to confirm which camera to switch to if multiple cameras exist.
If you’d like, tell me:
• Which LiveKit client & version you’re using (web, React, native), and
• How you currently create/publish the camera track (a short code snippet)
I can adapt the example exactly to your setup and help handle edge cases (multiple cameras, permission prompts, or fallback images).
Sources: client-sdk-js | livekit/livekit_composite | DeepWiki | examples/other | livekit/livekit_composite | DeepWikifresh-television-67623
02/18/2026, 4:06 PMasync change_camera(cam_id: string) {
if (!this.participant) {
return;
}
if (!this.cam_on) {
this.cam_id = cam_id;
return;
}
this.cam_id = cam_id;
try {
await this.participant.setCameraEnabled(false);
await this.participant.setCameraEnabled(true, {
deviceId: { exact: this.cam_id }
});
if (this.room.getActiveDevice('videoinput') !== this.cam_id) {
await this.room.switchActiveDevice('videoinput', this.cam_id);
}
this.participant.videoTrackPublications.forEach(e=>e.videoTrack?.restartTrack({
deviceId: { exact: this.cam_id }
}));
} catch (error) {
console.error('Error changing camera:', error);
}
}tall-airline-16756
02/18/2026, 4:07 PMdeviceId no longer exists because the camera was unplugged or the device list changed.
• In your code you: disable and immediately re-enable the camera, then call switchActiveDevice and restartTrack. Multiple concurrent requests and using a stale deviceId can produce conflicting requests and cause OverconstrainedError.
• Restarting tracks manually while also calling setCameraEnabled(true) duplicates work and may produce race conditions.
Recommended approach (what to change)
• Verify the requested camera deviceId exists before trying to use it.
• Wait for the camera to fully stop before trying to reacquire a device.
• Use one single flow to re-acquire the camera (either use setCameraEnabled(true, { deviceId }) or manually create a new LocalVideoTrack and call replaceTrack()), don’t do both at once.
• Provide a graceful fallback when the exact device isn’t available (e.g., pick any available camera).
Safer implementation you can try (TypeScript / LiveKit)
1. Check available devices with room.getMediaDevices('videoinput') (or navigator.mediaDevices.enumerateDevices()).
2. If the requested cam_id exists, proceed; otherwise pick a fallback.
3. Disable camera, wait for that to complete, then enable with the device constraint OR create a new track and replace.
Example pattern (adapt to your app):
async change_camera(cam_id: string) {
if (!this.participant) return;
// If camera is off, just set the id for later
if (!this.cam_on) {
this.cam_id = cam_id;
return;
}
// Ensure device is currently available
const videoDevices = await this.room.getMediaDevices('videoinput');
const device = videoDevices.find(d => d.deviceId === cam_id);
// If requested device is gone, pick any available camera as fallback
const targetDeviceId = device ? cam_id : (videoDevices[0]?.deviceId ?? null);
if (!targetDeviceId) {
console.error('No camera available to switch to.');
return;
}
this.cam_id = targetDeviceId;
try {
// Stop current camera and wait
await this.participant.setCameraEnabled(false);
// Option A: Let LiveKit re-acquire using setCameraEnabled (recommended for simple flows)
await this.participant.setCameraEnabled(true, {
deviceId: { exact: targetDeviceId }
});
// Optionally: ensure room.active device is updated
if (this.room.getActiveDevice('videoinput') !== targetDeviceId) {
await this.room.switchActiveDevice('videoinput', targetDeviceId);
}
// Do NOT also call restartTrack() here — setCameraEnabled handles it.
} catch (error: any) {
console.error('Error changing camera:', error);
// Handle OverconstrainedError with fallback
if (error?.name === 'OverconstrainedError' || error?.message?.includes('Overconstrained')) {
console.warn('Requested camera unavailable; falling back to any available camera.');
try {
// Fallback: enable camera without exact deviceId to pick default
await this.participant.setCameraEnabled(true);
} catch (err) {
console.error('Fallback camera enable failed:', err);
}
}
}
}
If you prefer manually creating and replacing the track (Option B)
• Create a new LocalVideoTrack with LocalVideoTrack.createCameraTrack({ deviceId: { exact: targetDeviceId } })
• Call room.localParticipant.replaceTrack(oldLocalTrack, newLocalTrack)
• This gives you finer control and avoids some built-in re-acquire logic; again, don’t call both.
Extra tips
• Listen for device changes proactively with navigator.mediaDevices.ondevicechange (or the SDK device change hooks) and update available device list accordingly.
• If the camera was physically unplugged, device IDs may change—always validate against enumerateDevices() before requesting by deviceId.
• Avoid simultaneous calls that ask the browser for media devices with conflicting constraints.
Quick questions to help me refine this for you
• Which LiveKit JS client version are you using?
• Which browser(s) are you testing on?
• Do you create your initial camera track with LiveKit APIs (e.g., setCameraEnabled or LocalVideoTrack.createCameraTrack)?
Give me those details and I’ll adapt the sample to match your SDK version and browser quirks.
Sources: Configuring the LiveKit Client SDK for Optimal Video Quality | LiveKit | An introduction to WebRTC Simulcast | Optimizing Video Calls for Low-Powered Devices | LiveKit | client-sdk-js | livekit/livekit_composite | DeepWiki | client-sdk-android | livekit/livekit_composite | DeepWiki