Switching between front and rear cameras on mobile web apps should be simple. Ideally, you'd fetch the list of available video devices, allow users to select one, and then update your getUserMedia() constraints accordingly.
On Android, this logic works as expected. But if you’ve tried the same on iOS—especially Safari—you’ve likely run into frustrating roadblocks:
deviceId often returns an empty string.
enumerateDevices() yields devices without useful labels.
Programmatic switching between front and back cameras frequently fails.
Here’s how I resolved it with a lightweight, unified solution that works reliably across both Android and iOS.
The Problem with iOS Browsers
iOS (especially Safari) restricts access to certain device metadata due to privacy concerns. This includes:
Missing or empty deviceId, label, and groupId
Unpredictable results from enumerateDevices()
Inconsistent support for selecting specific devices via constraints
Because of this, the usual method of toggling cameras using deviceId falls short on iOS devices.
The Cross-Platform Solution
The workaround leverages conditional logic to tailor camera switching based on the user's platform:
On Android: Enumerate video input devices and cycle through them using their deviceId.
On iOS: Toggle between front (user) and rear (environment) cameras by adjusting the facingMode.
JavaScript Implementation
Here’s the complete code to support both platforms:
javascriptCopyEdit
let currentStream = null; let videoDevices = []; let deviceIndex = 0; let count = 2; // For toggling facingMode on iOS const video = document.getElementById("video"); const switchBtn = document.getElementById("switchBtn"); const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent); async function startCamera(deviceId = null) { if (currentStream) { currentStream.getTracks().forEach((track) => track.stop()); } const constraints = isIOS ? { video: { facingMode: count % 2 === 0 ? "environment" : "user", }, audio: false, } : { video: deviceId ? { deviceId: { exact: deviceId } } : true, audio: false, }; try { const stream = await navigator.mediaDevices.getUserMedia(constraints); currentStream = stream; video.srcObject = stream; await video.play(); } catch (err) { console.error("Camera error:", err); alert("Could not access the camera."); } } async function getVideoDevices() { const devices = await navigator.mediaDevices.enumerateDevices(); videoDevices = devices.filter((d) => d.kind === "videoinput"); } switchBtn.addEventListener("click", async () => { if (videoDevices.length <= 1) { if (isIOS) { count++; await startCamera(); } else { alert("No other camera available."); } return; } deviceIndex = (deviceIndex + 1) % videoDevices.length; await startCamera(videoDevices[deviceIndex].deviceId); }); (async () => { await getVideoDevices(); await startCamera(videoDevices[deviceIndex]?.deviceId); })();
Key Benefits of This Approach
Works on iOS Safari and Chrome (tested on iPhone)
Works on Android Chrome and Firefox
No external libraries or frameworks required
Graceful fallback for single-camera devices
Clean, minimal, and production-friendly
Why This Works
This hybrid approach plays to the strengths of each platform:
Android gives full access to media device metadata, so switching by deviceId is reliable.
iOS respects privacy by obfuscating device details, but reliably honors the facingMode constraint—so toggling that allows for effective switching.
Final Thoughts
Whether you’re building a photo capture utility, document scanner, or browser-based video tool, ensuring consistent camera toggling is key to a smooth user experience.
With this lightweight yet powerful approach, you can offer users a professional, reliable camera switch experience — without resorting to messy platform-specific workarounds or native SDKs.