Compatibility guide
Which video files actually open in a browser
· 16 min read
Almost every frame extraction problem that gets blamed on the extractor starts one step earlier: the browser could not decode the file. The page loaded, the drop zone accepted the file, and then nothing happened — or the first frame appeared and every later one was grey.
That failure is not mysterious, but it is invisible from the outside, because a file name carries no information about what is inside it. This guide separates the three layers that people collapse into one (container, video codec, audio codec), explains the two checks a browser performs before it will show you anything, and gives you a probe you can paste into a console to find out exactly which check your file failed.
In short
- A
.mp4extension describes a box, not a codec. The same extension can hold H.264, HEVC, AV1 or ProRes. - A browser needs two separate things: a demuxer that recognises the container, and a decoder for the codec inside it. Either one missing means no playback.
- The decoder usually lives in the operating system, not in the browser. The same Chrome build decodes HEVC on one machine and refuses it on another.
- For a local file,
MediaErrorcode 4 means “no decoder”, not “corrupt file”. Code 3 is the one that means the data itself broke. - Re-encoding from the original master fixes compatibility. Re-encoding from a screen recording fixes compatibility and destroys the detail you were trying to capture.
Contents
- 1. A file extension is not a codec
- 2. The browser runs two checks, in this order
- 3. What each browser family can actually decode
- 4. One Chromium build, seventeen files, measured
- 5. Hardware decoding is the variable nobody checks
- 6. Test the file in front of you, not the extension
- 7. Reading the error code
- 8. Symptom to cause, in one table
- 9. Converting instead of fighting
- 10. Why this matters for frame extraction specifically
A file extension is not a codec
People say “MP4” to mean three different things. It is worth separating them once, because every diagnostic decision below depends on which layer you are actually looking at.
- The container is the box: how video, audio, subtitles and metadata are interleaved into one file. MP4 and MOV are both ISO Base Media File Format. WebM is a restricted profile of Matroska. MKV is Matroska.
- The video codec is the compression scheme for the picture: H.264 (AVC), HEVC (H.265), VP9, AV1, ProRes, MPEG-2, MPEG-4 Part 2.
- The audio codec is separate again: AAC, Opus, AC-3, PCM, MP3.
A browser has to handle all three. It can demux the container, decode the audio, and still show you a black rectangle, because the video decoder for that profile is missing. This is why “it plays but there is no picture” is a real and common symptom rather than a contradiction.
| Container | Common contents | Typical origin |
|---|---|---|
| MP4 (.mp4, .m4v) | H.264 + AAC, HEVC + AAC, AV1 + Opus | Phones, cameras, screen recorders, social exports |
| MOV (.mov) | H.264 + AAC, ProRes + PCM, HEVC + AAC | iPhone, Final Cut Pro, macOS screen recording |
| WebM (.webm) | VP8 or VP9 + Opus, AV1 + Opus | Browser recordings, web exports, Wikimedia |
| MKV (.mkv) | Anything. H.264, HEVC, AV1, multiple audio tracks, subtitles | Rips, OBS output, archiving |
If you have the file on a machine with FFmpeg installed, one command settles the question before you touch a browser:
ffprobe -v error \
-show_entries stream=index,codec_type,codec_name,profile,pix_fmt,width,height,r_frame_rate \
-of json input.mp4codec_name is the decoder you need. profile matters more than people expect: HEVC Main10 is a different decoder path from HEVC Main, and ProRes 422 HQ is a different path from ProRes Proxy. A file can be “HEVC” and still fail where another HEVC file plays.
The browser runs two checks, in this order
When you assign a source to a <video> element, the browser does not try everything and hope. It walks a fixed sequence, and the first failure stops it.
- Sniff or trust the declared type, then pick a demuxer. If no demuxer claims the container, playback ends here with
MEDIA_ERR_SRC_NOT_SUPPORTED. - Ask the media pipeline for a decoder that matches the codec, profile, resolution and bit depth. If the platform has no such decoder, playback also ends here, with the same error code.
- Only then does it start reading samples, and only then can you get a decode error (
MEDIA_ERR_DECODE) from damaged data.
The practical consequence is that code 4 covers two very different situations — wrong box, or missing decoder — and the browser will not tell you which. That is the single most useful thing to understand about these errors, because the two situations have completely different fixes.
It also explains the MKV confusion. An MKV containing H.264 and AAC is, codec-wise, identical to an MP4 containing H.264 and AAC. It fails anyway in most browsers, because step one failed: no demuxer claimed the container. Nothing is wrong with the video inside. Firefox has shipped partial Matroska support limited to H.264 video with AAC audio, which is exactly the shape of a “make the container work first” fix.
What each browser family can actually decode
The table below summarises vendor-documented behaviour as of October 2026. Read the caveat column before you read the verdict column: several cells are “yes, if the operating system provides a hardware decoder”, which is not the same kind of yes as H.264.
| Codec | Usual container | Chromium (Chrome, Edge) | Safari | Firefox | What decides it |
|---|---|---|---|---|---|
| H.264 (AVC) | MP4, MOV | Yes | Yes | Yes | Bundled software decoder. Effectively universal, including 8-bit 4:2:0 at 4K. |
| HEVC (H.265) | MP4, MOV | Only with a platform decoder | Yes | Recent versions, platform-dependent | Chromium delegates to the OS. On a machine with no HEVC hardware or OS codec, the file will not open at all. |
| VP9 | WebM, MP4 | Yes | Yes on macOS, more limited on iOS | Yes | Software decoder is bundled by Chromium and Firefox; Apple support arrived with WebM on macOS Big Sur and later. |
| AV1 | MP4, WebM | Yes | Yes on hardware that has an AV1 decoder | Yes | Software decode is viable in Chromium and Firefox; Apple gates it to Apple silicon generations that include the decoder. |
| ProRes 422 / 4444 | MOV | No | Yes | No | Apple-only decoder. This is the clearest “works on one machine, not another” case in the table. |
| MPEG-4 Part 2 (DivX, Xvid) | AVI, MP4 | No | No | No | Dropped from every modern browser. Old files must be transcoded. |
| MPEG-2 | MPG, TS | No | No | No | Broadcast and DVD format. Browsers never supported it for web playback. |
One Chromium build, seventeen files, measured
Documentation tells you what should happen. It is more useful, when a file refuses to open, to know what one real browser did with a set of files whose contents are known exactly. So we generated seventeen three-second clips from one synthetic source — same picture, same duration, different container and codec — and put each of them through Google Chrome for Testing 145.0.7632.6, headless, on macOS on Apple silicon.
The procedure for each file: load it into a <video>, seek to the midpoint, wait for seeked, draw the frame to a canvas, and read the pixels back. A decoded frame of this source is never a flat colour, so a flat canvas is unambiguous evidence that no frame was produced. That last step is the one that matters, because it separates two things people collapse into “it plays”.
| File | Codec and profile | <video> opened | Frame drawn | Detectors said |
|---|---|---|---|---|
| h264.mp4 | H.264 High, 8-bit | yes | yes | yes |
| h264.mov | H.264 High, 8-bit | yes | yes | no |
| h264.mkv | H.264 High, 8-bit | yes | yes | yes |
| h264-10bit.mp4 | H.264 High 10 | yes | yes | yes |
| hevc.mp4 | HEVC Main, 8-bit | yes | yes | yes |
| hevc.mov | HEVC Main, 8-bit | yes | yes | no |
| hevc.mkv | HEVC Main, 8-bit | yes | yes | yes |
| hevc-10bit.mp4 | HEVC Main 10 | yes | yes | yes |
| vp9.webm | VP9 Profile 0 | yes | yes | yes |
| vp9.mkv | VP9 Profile 0 | yes | yes | yes |
| vp9.mp4 | VP9 Profile 0 | yes | yes | yes |
| av1.webm | AV1 Main | yes | yes | yes |
| av1.mp4 | AV1 Main | yes | yes | yes |
| prores422.mov | ProRes 422 Standard | yes | no | no |
| mpeg4.mp4 | MPEG-4 Part 2 Simple | yes | no | no |
| mpeg2.mpg | MPEG-2 Main | no | — | no |
| mjpeg.avi | Motion JPEG | no | — | no |
Thirteen of seventeen produced a frame. Two opened and never produced one. Two were refused outright with MediaError code 4 — MPEG-2 in a program stream, and Motion JPEG in AVI — and in both cases the container was the reason, not the codec.
The row that should change how you write feature detection is h264.mov. Chrome decoded it, sought it, and drew it, and both MediaCapabilities.decodingInfo() and canPlayType() said no. They said no because the question they answer is not “can you play this file” but “do you recognise this container MIME type”, and Chromium does not list video/quicktime as a supported type — whatever is inside it. The same H.264 stream in an MP4 got probably from the same call. Six of the seventeen files drew a “no” from both detectors; two of those six played perfectly.
HEVC decoded here, and that is not a contradiction of the table above. macOS supplies an HEVC decoder, so Chromium used it. The same build on a Linux machine without the codec would answer differently on the same file. This is the platform-decoder claim from the previous section, observed rather than asserted — and the reason a compatibility table is only meaningful with a machine attached to it.
Two smaller numbers from the same run. Seeking to the midpoint took between 3 and 48 ms, median 27 ms. The very first seek of the session took 384 ms, which is why we discard a warm-up load before measuring: quote that first number and every file looks slow.
And requestVideoFrameCallback produced a timestamp for none of the fifteen successful seeks. The callback fires when a frame is presented to the compositor, and a paused element that has just been seeked does not reliably present one. Any code that waits on it to know a seek is finished will wait forever; the drawn pixels are the signal that actually arrives.
Hardware decoding is the variable nobody checks
For H.264 the browser ships its own decoder, so playback is predictable. For HEVC, and increasingly for AV1, the browser asks the operating system for a decoder, and the operating system asks the GPU driver. That chain breaks silently in ordinary situations:
- A virtual machine or remote desktop session often has no video acceleration at all, so HEVC and AV1 fail while H.264 works.
- An older GPU may decode 8-bit HEVC but not 10-bit HEVC, so one phone recording opens and another does not.
- Chrome on Linux usually has no HEVC decoder, because the common Linux distributions do not ship one.
- A browser launched with hardware acceleration disabled falls back to software, which may not exist for that codec.
You can see the real answer instead of guessing. In Chromium browsers, open chrome://gpu and search the page for hevc and av1; the Video Acceleration Information section lists exactly which decode profiles are available. In Firefox, open about:support and read Codec Support Information. On macOS, Safari uses the system VideoToolbox framework, so the question becomes whether the OS supports that profile on that hardware.
This is also why “it worked on my other computer” is a legitimate report and not a user error. It is the same browser, the same file, and a different decoder.
Test the file in front of you, not the extension
The probe below does four things a canPlayType call alone cannot: it loads the file, reads the real duration and pixel dimensions, seeks to the midpoint, and reports the error object if any step fails. Paste it into the console of any page on this site, or of a blank tab, after selecting a file with a file input.
const probe = async (file) => {
const url = URL.createObjectURL(file);
const v = document.createElement('video');
v.preload = 'metadata';
v.muted = true;
const out = {
name: file.name,
declaredType: file.type || '(none)',
bytes: file.size,
canPlayType: v.canPlayType(file.type || '') || '(empty string)',
};
try {
v.src = url;
await new Promise((ok, fail) => {
v.onloadedmetadata = ok;
v.onerror = () => fail(v.error);
});
out.duration = v.duration;
out.size = `${v.videoWidth}x${v.videoHeight}`;
v.currentTime = v.duration / 2;
await new Promise((ok, fail) => {
v.onseeked = ok;
v.onerror = () => fail(v.error);
});
out.seek = `ok, landed at ${v.currentTime.toFixed(3)}s`;
} catch (err) {
out.error = err && err.code
? { code: err.code, message: err.message }
: String(err);
} finally {
URL.revokeObjectURL(url);
}
return out;
};
// In a page with <input type="file" id="f">:
console.table(await probe(document.querySelector('#f').files[0]));Three results are worth reading carefully. canPlayType returning an empty string means “I have no opinion”, which for a container the browser does not know is the same as no. duration of Infinity or NaN means the index is missing or the file was never finalised. A seek that succeeds but lands a few hundred milliseconds away from where you asked is normal and is covered in the accuracy guide.
Reading the error code
| Code | Constant | Meaning | What to do |
|---|---|---|---|
| 1 | MEDIA_ERR_ABORTED | Fetching stopped because something cancelled it — usually your own code, or a source change mid-load. | Not a file problem. Check that you are not reassigning src while a load is in flight. |
| 2 | MEDIA_ERR_NETWORK | A network error occurred after the resource was selected. | Should not happen for a blob URL. If it does, the blob was revoked before playback finished. |
| 3 | MEDIA_ERR_DECODE | A decoder existed and was chosen, but the bitstream broke while decoding. | The file is damaged, truncated, or was written by a tool that produced an invalid stream. Re-export from the source. |
| 4 | MEDIA_ERR_SRC_NOT_SUPPORTED | No demuxer claimed the container, or no decoder matched the codec and profile. | The common case. Identify the codec with ffprobe, then transcode to H.264 in MP4. |
Code 4 is the one that gets misdiagnosed as “corrupt file”. A perfectly healthy ProRes file on a Windows machine in Chrome returns code 4, and the bytes are fine. Code 3 is the one that actually indicates damage, and it is comparatively rare.
Symptom to cause, in one table
| Symptom | Most likely cause | Fix |
|---|---|---|
| Audio plays, picture stays black | Container and audio decode, video decoder missing. Classic HEVC-in-MP4 on a machine without the OS codec. | Transcode the video track to H.264; keep the audio as AAC. |
| Duration shows 0:00 or Infinity | Index missing or file not finalised — an interrupted recording, or a fragmented MP4 that was never closed. | Remux with ffmpeg -i in.mp4 -c copy out.mp4. If the moov atom is gone, the file is unrecoverable. |
| Opens, but seeking hangs or jumps back | Very long GOP, or a damaged index. The decoder has to walk forward from a distant keyframe. | Remux, or re-encode with a shorter keyframe interval. |
| First frame fine, later frames grey or green | Decode errors from damaged data in the middle of the stream. | Re-export from the source. This one really is corruption. |
| Works in Safari, fails in Chrome | ProRes, or HEVC on a machine without the decoder. | Transcode to H.264. |
| Works in Chrome, fails in Safari | VP9 or AV1 in a profile Safari does not decode on that hardware, or Opus in an MP4. | Transcode to H.264 + AAC in MP4. |
Converting instead of fighting
If the codec is not decodable, no browser setting will change that, and no amount of retrying in the extractor will either. A working copy is the answer. Two rules make the difference between a working copy that is useful and one that quietly ruins your source.
- Always transcode from the highest-quality file you have, not from a screen recording of it. Every generation of re-encoding removes detail, and frame extraction makes that loss visible at 100 percent zoom.
- Keep the original. The working copy exists so the browser can decode; it is not a replacement for the master, and it should never be the only copy you keep.
ffmpeg -i input.mkv \
-c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p \
-c:a aac -b:a 160k \
-movflags +faststart \
working.mp4-crf 18is visually near-lossless for most footage. Going lower inflates the file for no visible gain; going to 23 starts to soften fine text and gradients.-pix_fmt yuv420pis not optional. A 10-bit or 4:2:2 source transcoded without it produces a file that some browsers decode and some do not — which is a new compatibility problem in place of the old one.-movflags +faststartmoves the index to the front of the file. It matters for streaming; for a local file it costs nothing and makes the file more robust to truncation.-preset slowbuys a smaller file at the same quality for more encode time. On a five-minute clip the difference is a coffee, not an afternoon.
If the problem is seek accuracy rather than decode, the keyframe interval is the lever:
ffmpeg -i input.mp4 \
-c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p \
-force_key_frames "expr:gte(t,n_forced*1)" \
-c:a copy \
seekable.mp4One keyframe per second is a heavy trade — the file grows substantially — but it makes every second of the timeline a seek target instead of every ten seconds. Use it when you are reviewing a clip repeatedly and the pause between click and frame has become the bottleneck. Do not use it to produce a delivery file.
Why this matters for frame extraction specifically
A video player hides decode problems by dropping frames and carrying on. A frame extractor cannot: it has to present one specific moment at full resolution, and it either has that moment or it does not.
- If the file cannot be demuxed, there is nothing to extract, and no setting changes that.
- If the file decodes but seeks poorly, extraction appears to hang. It is not hung; it is walking forward from a distant keyframe.
- If the source was itself produced by re-encoding a re-encode, the still will carry the accumulated softness. Extracting at original resolution preserves what is there; it does not restore what was removed.
- A 720p source cannot yield a true 4K still. Upscaling produces a larger file with the same amount of detail, which is why this site does not offer it.
The next guide takes the second point apart: what a browser actually does when you ask for a specific timestamp, and why the four extraction modes fail in different ways.
Questions this guide gets asked
Why does the same MP4 file play on one computer and not another?
Because the decoder is often supplied by the operating system rather than bundled with the browser. This is typical for HEVC and for AV1: Chromium asks the platform for a decoder, and a virtual machine, a remote desktop session, an older GPU, or a Linux install without the codec will answer no. H.264 is the exception — it is bundled and behaves the same everywhere.
Does converting a file to MP4 guarantee it will open?
Only if the conversion also changes the codec. Remuxing — copying the streams into a new container with -c copy — fixes container problems and leaves codec problems untouched. An HEVC stream copied into an MP4 is still HEVC. Use -c:v libx264 when the goal is compatibility.
What does “no decoder” look like in practice?
The video element fires an error event whose MediaError.code is 4, MEDIA_ERR_SRC_NOT_SUPPORTED, and canPlayType usually returns an empty string for that type. There is no partial playback and no fallback frame — the element simply never reaches loadedmetadata.
Is an MKV with H.264 inside the same as an MP4 with H.264 inside?
In codec terms, yes. In browser terms, no. The demuxer is a separate component from the decoder, and most browsers have no Matroska demuxer exposed to the video element. The picture data is identical; the box around it is not recognised.
Can a browser extract frames from a file it cannot play?
No. Frame extraction through a <video> element requires the browser to decode the stream, which is the same capability that playback requires. A server-side tool with its own decoder can handle more formats, but it has to receive the file, which is a different privacy trade-off from the local processing this site does.