DEV Community

Alex Neamtu
Alex Neamtu

Posted on • Originally published at sendrec.eu

How We Added Camera Recording for Mobile Browsers

Screen recording on mobile doesn't work. getDisplayMedia — the API that powers screen capture — is unavailable on iOS Safari and most mobile browsers. For the first version of SendRec, we showed a "not available" message and suggested users switch to a desktop browser.

That's a dead end for anyone who wants to record a quick video message from their phone. The fix: offer camera recording as an alternative. Users can record themselves talking with their phone camera, and the result goes through the same upload-and-share flow as a screen recording.

The problem with getDisplayMedia on mobile

The gap isn't a bug — it's a platform limitation. iOS doesn't expose screen capture to web apps. Android Chrome has partial support, but it's inconsistent. The browser API simply isn't there.

But getUserMedia — the camera and microphone API — works everywhere. Every phone that runs a modern browser can access its front and back cameras through this API. It's the same API that powers video calls in the browser.

The question was whether to extend the existing Recorder component or build something new.

New component vs. extending the existing one

The existing Recorder component is tightly coupled to screen capture. It manages getDisplayMedia, canvas compositing for drawing annotations, webcam overlay, pause/resume, and MediaRecorder — all in one component with a dedicated compositing hook. Adding camera recording logic into it would mean branching on "is this a camera or a screen?" throughout the component.

We built a separate CameraRecorder component instead. It shares the same onRecordingComplete callback interface, so the parent Record page doesn't care which one it renders:

interface CameraRecorderProps {
  onRecordingComplete: (blob: Blob, duration: number) => void;
  maxDurationSeconds?: number;
}
Enter fullscreen mode Exit fullscreen mode

The Record page checks which APIs are available and renders the right component:

const screenRecordingSupported =
  typeof navigator.mediaDevices?.getDisplayMedia === "function";
const cameraSupported =
  typeof navigator.mediaDevices?.getUserMedia === "function";

// In the render:
{screenRecordingSupported ? (
  <Recorder
    onRecordingComplete={handleRecordingComplete}
    maxDurationSeconds={limits?.maxVideoDurationSeconds ?? 0}
  />
) : (
  <CameraRecorder
    onRecordingComplete={handleRecordingComplete}
    maxDurationSeconds={limits?.maxVideoDurationSeconds ?? 0}
  />
)}
Enter fullscreen mode Exit fullscreen mode

Desktop users get screen recording. Mobile users get camera recording. Both produce a video blob that goes through the same upload path.

Camera preview on mount

Unlike screen recording — where you click a button and the browser shows a permission dialog — camera recording starts the preview immediately on mount. The component calls getUserMedia as soon as it renders:

useEffect(() => {
  async function startPreview() {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: { facingMode, width: { ideal: 1280 }, height: { ideal: 720 } },
      audio: true,
    });
    streamRef.current = stream;
    if (videoRef.current) {
      videoRef.current.srcObject = stream;
      videoRef.current.play().catch(() => {});
    }
  }
  startPreview();
}, [facingMode, stopStream]);
Enter fullscreen mode Exit fullscreen mode

The browser shows its own camera permission prompt. Once granted, the user sees a live preview of themselves. The "Start Recording" button sits below the preview — no second permission prompt needed when they tap it, because the stream is already active.

We request audio upfront too. Separating camera and microphone permissions would mean two prompts, and the second one would interrupt the user mid-recording if we waited.

Front camera mirroring

Front-facing cameras show a mirrored preview by default on native camera apps. This is what people expect — it matches a mirror, so when you move your hand left, the preview moves left. But the recorded video is not mirrored. When someone else watches the video, text on your shirt reads correctly and your movements match reality.

We replicate this with CSS:

<video
  style={{
    transform: facingMode === "user" ? "scaleX(-1)" : "none",
  }}
/>
Enter fullscreen mode Exit fullscreen mode

scaleX(-1) flips the preview element horizontally. The MediaRecorder records from the original stream, not the flipped preview element, so the output is unmirrored. When the user switches to the back camera (facingMode: "environment"), the mirror transform is removed.

Flipping cameras

The flip button toggles facingMode between "user" (front) and "environment" (back). When facingMode changes, the useEffect above runs again — it stops the old stream's tracks and calls getUserMedia with the new facing mode.

One detail: we disable the flip button during recording. Switching cameras mid-recording would mean stopping the current stream and starting a new one, which would either create a gap in the video or require splicing two streams together. Neither is worth the complexity. If you want to switch cameras, stop the current recording and start a new one.

MP4 over WebM

The original Recorder component produces WebM files. MediaRecorder defaults to WebM on Chrome, and that's what most desktop browsers support.

But Safari can't play WebM. A WebM file recorded on Chrome and shared with someone on Safari results in a blank player. This was already a known issue on desktop — we show a warning message. On mobile, where Safari is the dominant browser, it's a dealbreaker.

The fix is to prefer MP4 when the browser supports it:

function getSupportedMimeType(): string {
  if (typeof MediaRecorder === "undefined") return "video/mp4";
  if (MediaRecorder.isTypeSupported("video/mp4")) return "video/mp4";
  if (MediaRecorder.isTypeSupported("video/webm;codecs=vp9,opus"))
    return "video/webm;codecs=vp9,opus";
  return "video/mp4";
}
Enter fullscreen mode Exit fullscreen mode

Safari's MediaRecorder produces MP4 natively. Chrome on Android supports both, and we pick MP4 first for cross-browser playback. The upload path reads blob.type to set the correct Content-Type header, and the backend stores whichever format it receives.

This required a small backend change too — the video creation endpoint previously hardcoded video/webm as the content type. We added a contentType field to the create request, defaulting to video/webm for backward compatibility, and validating that only video/webm and video/mp4 are accepted.

Mobile-first UI

The camera recorder UI is designed for phones:

  • Full-width preview with rounded corners, maxing out at 480px on larger screens
  • 44px minimum touch targets on all buttons (Apple's recommended minimum)
  • Flip button overlaid on the preview (top-right corner, semi-transparent background)
  • Timer with remaining time below the preview during recording
  • Pause/Resume and Stop buttons in a wrapping flex layout

The controls use flexWrap: wrap so they stack vertically on narrow screens instead of overflowing. The recording indicator pulses red when active and turns grey when paused.

What we skipped

Camera recording is intentionally simpler than screen recording:

  • No drawing annotations — there's no screen content to annotate. You're recording yourself talking.
  • No webcam overlay — the camera IS the recording. There's no second video layer to composite.
  • No canvas compositingMediaRecorder records directly from the getUserMedia stream. No intermediate canvas, no requestAnimationFrame loop, no frame copying. This means better performance and lower battery drain on mobile.

The simplicity is the point. A screen recording tool needs to capture a display, composite overlays, and produce a merged output. A camera recording tool just needs to point a camera at someone and record. Keeping the components separate means neither carries the other's complexity.

Try it

Open app.sendrec.eu/record on your phone. If your browser supports screen recording, you'll get the full screen recorder. If not, you'll get the camera recorder with front/back switching, pause/resume, and the same share-link flow.

SendRec is open source (AGPL-3.0) and self-hostable. The camera recorder is in CameraRecorder.tsx.

Top comments (0)