JavaScript MediaRecorder Table
| Piece | What it does | Field note |
|---|---|---|
new MediaRecorder(stream, opts) | The wrapper | takes any live MediaStream; opts: mimeType plus audio/videoBitsPerSecond; read rec.mimeType back - some engines silently fall back |
MediaRecorder.isTypeSupported(m) | The honest probe | codec strings carry parameters (video/webm;codecs=vp9,opus); browsers disagree, probing beats user-agent sniffing |
start() vs start(ms) | The chunking choice | no argument: one giant blob at stop; with ms: periodic chunks - memory ceiling, progressive upload, crash survival |
ondataavailable | The chunk faucet | e.data per chunk; keep the array in order - the first chunk carries the container header every later chunk needs |
pause() / resume() | The gap killer | keeps one file across pauses; state walks recording-paused-recording; no new header on resume |
state + onerror | The status line | inactive, recording or paused; stop() flushes the final chunk before onstop fires |
Blob assembly | The output | new Blob(chunks, {type: rec.mimeType}) then URL.createObjectURL - feed video.src or an anchor download, revoke after use |
the codec matrix | The reality | Chromium family: WebM with VP8/VP9 + Opus (MP4 accepted in newer builds); Safari: MP4 with H.264 + AAC; one universal string does not exist |
MediaRecorder is the API that turns a live MediaStream into a recorded file, entirely in the browser: hand it the stream from getUserMedia (camera and mic), getDisplayMedia (the screen), canvas.captureStream, or a WebAudio MediaStreamDestination, and it captures, encodes and muxes - all three jobs - with no library and no server. The output arrives not as one file but as a sequence of chunks delivered over time, which is the detail that shapes every real integration.
Bottom line: two decisions define the recording. The codec string - probe with MediaRecorder.isTypeSupported, because browsers disagree (the Chromium family records WebM with VP8/VP9 video and Opus audio, Safari records MP4 with H.264 and AAC; recent Chromium also accepts video/mp4), and renaming the file extension never transcodes anything. The chunking - start() with no argument buffers everything and hands you one giant blob at stop; start(1000) delivers a chunk every second, which caps memory on long recordings, enables progressive upload, and keeps the chunks you already have when the tab dies. Chunks arrive in ondataavailable in order: keep the array in order and concatenate it into one Blob at the end.
Know the layer you are on: MediaRecorder is the convenience layer - it muxes for you, picks sane defaults, and asks for almost nothing. WebCodecs is the control layer - frame-level access, explicit codec parameters, and your own muxing. Start with MediaRecorder; the honest escalation to WebCodecs happens only when a concrete need appears: custom pipelines, per-frame processing, or containers the browser muxer does not offer.
How to use
- Probe before you construct: const mime = ['video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/mp4', 'video/webm'].find(t => MediaRecorder.isTypeSupported(t)); - order the list by your preference and take the first the browser honestly supports.
- Wrap the stream: const rec = new MediaRecorder(stream, {mimeType: mime, videoBitsPerSecond: 2500000, audioBitsPerSecond: 128000}); - an unsupported mimeType throws NotSupportedError here, which is why the probe comes first; unsupported values fall back rather than fail in some engines, so read rec.mimeType back to know what you actually got.
- Choose the chunking: rec.start(1000) for one chunk per second - the memory ceiling, live-delivery and crash-survival option - or rec.start() for the simplest one-blob-at-stop flow that risks all of the above.
- Collect chunks in order: rec.ondataavailable = e => { if (e.data && e.data.size) chunks.push(e.data); }; - the first chunk carries the container header and every later chunk depends on it, so dropping or reordering any chunk corrupts the file.
- Finish and assemble: rec.onstop = () => { const blob = new Blob(chunks, {type: rec.mimeType}); const url = URL.createObjectURL(blob); }; - one object URL feeds video.src or an anchor with the download attribute; revokeObjectURL when done, and remember stop() flushes the final chunk before firing stop.
Frequently asked questions
My recording plays in Chrome but not Safari - what is actually wrong?
Almost always the container-codec pair, and the fix is probing, not guessing. The engines still disagree: the Chromium family historically records WebM (VP8 or VP9 video, Opus audio), Safari records MP4 (H.264 video, AAC audio), and recent Chromium accepts video/mp4 too - but playback support on the receiving device is a separate question from what the recorder produced. Three consequences worth internalizing. First, build a preference list and probe each entry with MediaRecorder.isTypeSupported - the probe is the only honest interface; user-agent sniffing misclassifies hybrid and updated browsers. Second, read rec.mimeType back after construction: some engines silently fall back when handed an unsupported string, so your file extension should come from what you actually recorded. Third, never fix this by renaming: a WebM renamed to .mp4 is still WebM, and strict players reject it on content, not extension. If one file must play everywhere, record with the lowest common denominator you can probe, or transcode server-side where a real transcoder can normalize the output.
Should I use a timeslice, or record one blob and stop?
Use a timeslice for anything that runs longer than a few seconds - the tradeoffs are all asymmetries that favor chunks. rec.start(1000) delivers a chunk every second: memory stays bounded because the chunks can be uploaded and released as they arrive (progressive upload - the recording is on the server before the user hits stop), and a tab crash or battery death forfeits only the seconds since the last chunk instead of everything. rec.start() with no argument holds the entire recording in memory until stop: fine for a thirty-second clip, a frozen phone for a ten-minute one, and a total loss on crash. Two facts about chunks prevent the classic bugs. Chunks are not standalone files - the first chunk carries the container initialization and later chunks reference it, so a middle chunk played alone is garbage; the unit of playback is the whole ordered array concatenated into one Blob. And ondataavailable can fire with an empty or zero-size payload at boundaries - the size check before pushing keeps the assembly clean.
Where do the audio and video come from, and can I mix sources?
The recorder consumes any MediaStream, and composing one is ordinary track surgery. Sources: getUserMedia for camera and mic, getDisplayMedia for screen (with or without system or tab audio where the browser offers it), canvas.captureStream for rendering anything into video, and MediaStreamAudioDestinationNode from WebAudio for processed or synthesized audio. To mix, you have two clean options: compose tracks - new MediaStream([videoTrack, audioTrack]) takes one track of each kind and the recorder muxes what it is given - or mix audio properly in WebAudio (createMediaStreamSource from each mic, route through a GainNode per participant into one MediaStreamDestination) and hand the recorder that single mixed stream. The classic recording bug lives here: a preview element and the recorder must share or deliberately split the same tracks - muting the preview with track.enabled = false also mutes the recording, because it is the same track object; if the monitor should be silent while the recording is not, mute the video element (element.muted), never the track.
When is MediaRecorder the wrong tool, and what does upgrading to WebCodecs change?
MediaRecorder is the wrong tool when you need control it never exposes. No frame access: you cannot touch, watermark, analyze or substitute individual frames - the pipeline is stream in, container out. Limited parameters: bitrate targets and the codec string are the whole control surface; no keyframe intervals, no rate-control modes, no resolution changes mid-stream. Muxer-bound output: you get the containers the browser ships, and no fragment control for streaming protocols. WebCodecs inverts all three: VideoEncoder/VideoEncoderConfig takes frames (from any source, including a canvas or a decoder), exposes exact codec settings, and hands you raw encoded chunks - with the tradeoff that muxing into a playable file is your job (a JS muxer library, or your own WebM/MP4 writer). The escalation is one-way and should stay rare: MediaRecorder covers upload-the-recording, and WebCodecs pays for itself exactly when frames, parameters, or containers become requirements - a decision to make per feature, not per codebase.