That message is a browser-level verdict, not a diagnosis: it means the or element fired an error event and the browser collapsed every possible cause into one sentence. The MediaError code underneath separates them, but the text does not show it to you. So the fastest path is to decide which of four layers failed β€” the URL, the network, the container/codec, or the delivery pipeline β€” because each layer has a different fix and only one of them is actually about file format.

This article walks the diagnosis in the order a browser encounters the problem. It covers the five error codes the spec defines, the encoding profiles Chrome, Firefox, and Safari actually accept, why headless browsers and mobile Safari fail differently, the server header mistakes that cause it, and the moment when a file plays perfectly by direct link but refuses to play inside your player.

The Message Is Generated From the MediaError Object, and the Code Tells You Where to Look

When an HTMLMediaElement fails, the browser sets mediaElement.error to a MediaError object. The code property is a number from 1 to 4 under the WHATWG HTML Living Standard, and message is a human-readable string the browser vendor writes. The sentence you are reading is essentially a fallback description attached to codes 2 and 4, which is why it feels so vague.

Here is what each code actually means in practice.

Code Constant Browser's claim What it usually is
1 MEDIA_ERR_ABORTED Fetching was aborted by user request A load() call or navigation interrupted an in-flight request
2 MEDIA_ERR_NETWORK A network error caused fetching to stop Timeout, dropped connection, CORS rejection, 4xx/5xx on a range request
3 MEDIA_ERR_DECODE Decoding failed after the resource was usable Corrupt container, truncated file, unsupported codec inside a supported container
4 MEDIA_ERR_SRC_NOT_SUPPORTED The resource or its MIME type is not supported Wrong MIME type, HLS on desktop Chrome without hls.js, file:// protocol, missing codec

Read the code before you touch the file. In Chrome DevTools, select the media element in the Elements panel and type $0.error.code in the Console. In Firefox, the same expression works in the Console with the element selected via the Inspector's element picker. That number cuts your search space by roughly three quarters in a single step.

Two practical details matter here. First, error.message is vendor-specific and unreliable: Chrome and Firefox phrase the same condition differently, so never branch your application logic on the string. Second, code can be overwritten by later events, so log it inside the error event handler rather than polling after the fact.

Reproduce the Failure Deliberately So the Console Confirms Which Code You Have

Random troubleshooting wastes time because you cannot tell whether a change fixed anything. Build a ten-line test page that isolates the element from your application, then watch the console. This takes about four minutes and will save you an hour.

1: Create a local file probe.html containing a single element and open it in the browser where playback fails. 2: Open DevTools, go to the Console, and paste const v=document.querySelector('video'); v.addEventListener('error',()=>console.log(v.error.code, v.error.message)); 3: Reload the page and read the first number printed. Note it down before doing anything else. 4: Switch the Network panel to "Preserve log" and reload again; find the media request and check its status code, Content-Type response header, and whether any 206 Partial Content responses appear. 5: Open the same URL directly in a new tab with no HTML around it. If it plays there but not in the element, you have a delivery or embedding problem, not a codec problem. 6: Repeat steps 1–3 in one other browser engine β€” Chrome, Firefox, or Safari β€” because a code that changes between engines is nearly always a format or MIME issue.

If step 5 shows the raw file playing but step 4 shows a 200 status with a text/html content type, stop: your server is returning an error page or a login page instead of the media bytes, and the browser is honestly reporting that what it received was not audio or video.

Error Code 4 Is the Format Message, and It Covers More Than Format

Code 4 gets blamed for the whole sentence because "format is not supported" appears in the text. In reality it fires for at least five distinct conditions, only one of which is genuinely an unsupported codec. Treating all five as encoding problems is the most common waste of time in this entire troubleshooting path.

The five conditions: a response with a missing or wrong Content-Type (browsers use the MIME type as the first gate for selection); a URL served over file:// or a mixed-content http:// page; an HLS .m3u8 manifest requested by desktop Chrome, which does not implement HLS natively; a codec the browser lacks even though the container is fine; and a source URL that returns a redirect to an HTML error page.

Network conditions can masquerade as code 4 too. A reverse proxy that strips the Range request header breaks seeking and can produce a not-supported error even for a valid MP4, because the browser cannot satisfy its byte-range assumptions. Similarly, a CDN that returns Content-Range headers inconsistently will produce code 4 in Safari and code 2 in Chrome for the identical file.

Practical rule: if the direct URL plays in a tab, the file is fine and you are looking at delivery. If it does not play in a tab either, you are looking at format or MIME. That single check routes you correctly in most cases.

Chrome, Firefox, and Safari Accept Different Format Lists, and Safari Is the Strict One

Container and codec support is not uniform, and the differences run in specific directions you can memorize. Building one file and assuming it plays everywhere is the root of a large share of these errors.

Chromium-based browsers (Chrome, Edge) handle MP4/H.264/AAC, WebM with VP8 or VP9, and Ogg with Vorbis or Opus. Firefox supports the same MP4 profile plus WebM and Ogg, with Opus and FLAC support that varies by version. Safari on macOS and iOS supports MP4/H.264/AAC and, from Safari 14 onward, WebM with VP8/VP9 on macOS β€” but iOS Safari support for WebM has historically lagged and should be confirmed on the maker's compatibility tables for your target iOS version rather than assumed.

The practical consequence is directional. If a file plays in Chrome but fails in Safari, suspect the container or a codec variant Safari rejects β€” for example an MP4 with an unusual audio codec, or a fragmented MP4 with an incompatible moov atom placement. If it plays in Safari but fails in Chrome, suspect a codec trade-off, since Safari will happily decode some formats Chrome treats as unsupported.

HLS is the sharpest example. Safari on iOS plays .m3u8 natively; desktop Chrome and Firefox do not, and require a JavaScript library such as hls.js to feed the segments into Media Source Extensions. Serving one HLS URL to every browser without that shim produces exactly this error message on desktop Chrome and works perfectly on an iPhone, which makes it maddening to reproduce.

Code 2 Points at the Server or Network, and Range Requests Are the Usual Suspect

When the code is 2, stop looking at encoders. The browser started fetching and something interrupted it. The three causes worth checking first are the HTTP status of the media request, the server's handling of Range headers, and cross-origin policy.

Seeking and progressive playback depend on partial content. A correctly configured server answers a media request with 206 Partial Content and an Accept-Ranges: bytes header once the browser starts scrubbing. Many static hosts, older application servers, and custom API routes return 200 OK with the entire file instead. Chrome will often still play, Firefox is less forgiving, and Safari can fail outright because it insists on range support for media.

CORS is the second common culprit. If the media is on a different origin from the page, the element needs crossorigin="anonymous" and the server must return Access-Control-Allow-Origin matching the page's origin. A missing header produces a network error in some engines and a not-supported error in others.

A third cause is short-lived signed URLs. Cloud storage presigned links typically expire β€” Amazon S3's SigV4 presigned URLs can be configured from one second up to seven days, with the expiry set at generation time. If your player retries after the window closes, the request returns 403 and the browser reports a network failure. Log the response status alongside the media error to catch this fast.

Mobile Safari and Headless Browsers Fail Differently, and Both Are Easy to Misread

Two environments produce this error for reasons that never appear on a desktop browser, and both are common in automated testing and mobile QA.

iOS Safari enforces a playback policy that desktop browsers do not. Inline playback of video requires the playsinline attribute, and audio playback initiated without a user gesture can be blocked or interrupted. A file that autoplays fine on desktop Chrome will fire an error event on iOS if the request is made before any touch, because the element never receives permission to fetch or decode. Adding playsinline and wiring playback to a tap resolves a large share of these reports.

Headless Chromium β€” the engine inside Puppeteer and Playwright β€” ships without proprietary codecs in many builds. That means an H.264/AAC MP4 that plays perfectly in a user's Chrome can fail with code 4 in your CI pipeline. This is a well-known trap for teams who add video smoke tests and then chase a phantom bug. Test playback in headed mode, or use a Chromium build with the codecs included, before concluding the file is broken.

A third environment worth naming: cross-origin iframes with restrictive sandbox attributes block media loading entirely, producing a network error in the console that has nothing to do with the file. If the player sits inside an embed, check the iframe's allow attribute for autoplay and encrypted-media before you re-encode anything.

Wrong Content-Type and Missing Range Support Cause Most Server-Side Cases

Two server behaviors cause the majority of cases that are genuinely the server's fault, and both are visible in response headers within seconds.

The first is a wrong or absent Content-Type. An MP4 served as application/octet-stream or text/plain will be rejected by some browsers before a single byte is decoded. The canonical values are video/mp4, audio/mpeg for MP3, audio/ogg, video/webm, application/vnd.apple.mpegurl for HLS manifests, and video/mp2t for HLS transport stream segments. Mapping these in nginx, Apache, or your CDN's config is a one-time fix that eliminates a whole class of reports.

The second is broken range handling. Verify it directly rather than trusting the config: request the URL with a Range: bytes=0-1023 header and confirm the response is 206 with a matching Content-Range and a body of exactly 1,024 bytes. If you get 200 and the whole file, range support is off. On nginx this is typically enabled when Accept-Ranges is not disabled and the file is served statically; behind an application server, you may need to implement or enable it explicitly.

A third header to audit is caching. A CDN that caches a truncated or failed response can serve the same broken bytes to every user for the life of the TTL, making a transient origin error look like a permanent format problem. Purge the path and reproduce against the origin before you change any encoding settings.

Encoding Settings That Produce Playable Files and the Ones That Quietly Break

If you have confirmed the code is 3 or 4 and the direct URL fails too, re-encoding is the right move β€” but the settings matter more than the tool.

Start with H.264 video in an MP4 container with AAC audio at 44.1 or 48 kHz. Use the -movflags +faststart flag in FFmpeg to move the moov atom to the front of the file so playback can start before the download finishes. Without it, a large file on a slow connection can appear to fail because the browser is waiting for metadata at the end of the file. Set the pixel format to yuv420p, the widely supported default, because some encoders produce yuv444p or yuv422p profiles that Safari rejects.

Match the level and profile to your audience. High profile H.264 is fine for desktop and modern mobile; baseline is the safest floor for very old devices. For audio-only content, MP3 in an audio/mpeg response is the most universally tolerated, with AAC in an M4A container as the modern alternative.

Then produce a second source. A single with two children β€” an MP4 first and a WebM second β€” lets each browser pick what it can decode, and the fallback text inside the element only appears when neither works. This costs one extra encode and removes almost all codec-related reports.

One failure mode deserves a warning. Truncated encodes β€” from a killed job, a full disk, or a streaming upload that dropped β€” produce files that download completely, look correct in a file manager, and fail with code 3. Check the file size against the expected output of the encode and validate with ffprobe before publishing.

When the File Plays by Direct Link but Not in Your Player, the Problem Is the Wrapper

This is the most common scenario behind a support ticket, and it is almost never the video. If pasting the URL into a browser tab plays it, the file, container, codec, MIME type, and server are all fine. The fault is in how your page requests it.

Work through the wrapper in this order. First, confirm the src or URL is byte-for-byte the same as the one you pasted β€” relative paths, a missing query string, or a signed token generated on the server but omitted in the client all break this. Second, check whether the media sits on a different origin: if so, the crossorigin attribute and Access-Control-Allow-Origin header both need to be right. Third, look for authentication: cookies or Authorization headers applied to a fetch will not be sent by a bare media element, so protected files must be fetched as a blob and then attached via URL.createObjectURL.

Fourth, check the element's attributes. A with a type attribute that does not match the actual content will be skipped by some browsers before the request is even attempted. A preload="none" element that never receives load() will not try. And a lazy-loading library that swaps the src attribute will frequently trigger errors on the initial empty value, which then get logged as if they were real failures.

Finally, consider the lazy-load timing. If your player sets src and immediately calls play(), and the request is rejected by the browser for reasons unrelated to format, the error event fires with code 2 while the file itself is perfect.

A Worked Diagnostic Order That Finds the Layer in Under Ten Minutes

The sequence below follows the order a browser encounters the problem, so each step eliminates a layer permanently. Keep it in this order and you will not re-check the same thing twice.

Step Check If it fails If it passes
1 Read error.code in DevTools Code 1: user aborted, ignore Note the number and continue
2 Open the media URL directly in a tab Format or MIME problem; go to step 6 Wrapper problem; go to step 5
3 Inspect response status and Content-Type Fix server MIME mapping Continue
4 Send Range: bytes=0-1023 request Enable range support on server Continue
5 Check crossorigin, signed URL expiry, and iframe sandbox Fix the wrapper attribute or refresh the token Re-encode (step 7)
6 Compare code across Chrome, Firefox, Safari Codec or container mismatch; go to step 7 Continue
7 Re-encode to H.264/AAC MP4 with +faststart and add a WebM fallback Escalate to the hosting provider with the error.code and response headers Confirm in all three browsers

The trick that saves the most time is step 2. A direct link test permanently splits "the file or its server" from "the page or its code", and those two halves share almost no troubleshooting steps. Everything after step 2 belongs to exactly one half.

A second efficiency gain comes from checking the response status alongside the media error every time you reproduce. Log fetch('HEAD', url) alongside the media element during development; a mismatch between the two is a reliable signal that the wrapper is the problem.

FAQ

Does this error mean the file is corrupted?

No β€” corruption is only one of five conditions behind code 3, and the same message covers codes 2 and 4. A truncated download from a killed encode will produce it, but so will a perfectly valid MP4 served with the wrong Content-Type. Run ffprobe on the file and check the response headers before you assume the bytes are damaged.

Why does the video play on my phone but not on my laptop?

HLS is the near-certain answer. Safari on iOS plays .m3u8 natively, while desktop Chrome and Firefox need a JavaScript library to feed segments into Media Source Extensions. Serve the HLS manifest to Safari and an MP4 or an hls.js-wrapped stream to everything else, and the split disappears.

Can this error come from a missing CORS header?

Yes, and it appears as code 2 in Chromium. A media element on one origin requesting from another needs crossorigin="anonymous" on the element and an Access-Control-Allow-Origin response header naming the page's origin. The error surfaces as a network failure even though the file is served fine to other clients.

Why does it fail in my automated browser tests but not for users?

Headless Chromium builds often omit proprietary codecs, so an H.264/AAC MP4 that plays for a real Chrome user reports code 4 in Puppeteer or Playwright. Test in headed mode or use a codec-enabled Chromium build before filing a bug against the file.

Do I need to add a second element?

You should, because one MP4 does not cover every engine. Ship an H.264/AAC MP4 as the first source and a WebM/Opus file second; the browser picks the first one it can decode and falls back automatically. This costs one extra encode and removes most cross-browser format reports.

What does -movflags +faststart actually change?

It relocates the moov metadata atom from the end of the MP4 to the front. Without it, a browser must download to the end of a large file before it can read the metadata and begin playback, which reads as a failure on slow connections. The flag is a single argument in FFmpeg and changes nothing else about the encode.

Is the error different for audio files?

The mechanism is identical β€” and share HTMLMediaElement and the same five MediaError codes. What differs is the MIME type (audio/mpeg rather than video/mp4) and the fact that iOS Safari's autoplay restrictions bite harder on audio, so gesture-initiated playback failures are more common than format failures.

Can a short-lived signed URL cause this?

It can, and it is easy to miss because the file works when you test it manually. Presigned cloud storage links expire β€” Amazon S3's SigV4 URLs can be configured from one second to seven days β€” so a player retrying after the window closes receives 403 and reports a network error. Log the HTTP status next to error.code to catch it.

If the direct URL plays fine, do I still need to re-encode?

No. A working direct link proves the container, codec, MIME type, and server are all acceptable to that browser, so re-encoding wastes time. Look instead at the src value, the crossorigin attribute, the signed token, and the iframe sandbox permissions.