For my graduation project I'm building a small page that re-encodes lecture clips in the browser, and I wanted an H.265 option because the files come out smaller. The WebCodecs docs make the check look like one line: pass a config to VideoEncoder.isConfigSupported() and read supported. I wrote a probe, ran it in six browser builds on my Mac, and the answers looked tidy until I actually encoded with them. A true turned out to mean at least three different things. What finally let me tell them apart was boring: encode the same 30 frames under every hint and put the byte counts and times next to each other.
I tested six browser builds on one Mac
Everything ran on an Apple M4 with macOS 26.5 on 2026-10-19. The builds were Chromium 149, Chrome 155, Firefox 151, a macOS WebKit 26.5 build (not Safari on an iPhone), a stripped-down build of the same Chromium 149, and Chromium 149 with hardware video encoding turned off, to stand in for a machine without a hardware encoder. I asked about HEVC Main and Main10, in both hvc1 and hev1 form, with all three hardwareAcceleration hints. Whenever the answer was true, I didn't stop there. I configured a real encoder, pushed 30 canvas frames I drew myself at 1920×1080, 30 fps and 4 Mbps, and wrote down the output bytes and the time. This is the core of the probe:
async function probeHevc(codec, hint) {
const probeCfg = { codec, width: 1920, height: 1080, bitrate: 4e6, framerate: 30, hardwareAcceleration: hint };
const { supported } = await VideoEncoder.isConfigSupported(probeCfg);
if (!supported) return { codec, hint, supported };
let bytes = 0;
const enc = new VideoEncoder({ output: (c) => { bytes += c.byteLength; }, error: console.warn });
enc.configure(probeCfg);
const cv = new OffscreenCanvas(1920, 1080), g = cv.getContext('2d');
const start = performance.now();
for (let i = 0; i < 30; i++) {
g.fillStyle = `hsl(${i * 12},60%,50%)`; g.fillRect(0, 0, 1920, 1080);
g.fillStyle = '#fff'; g.font = '80px sans-serif'; g.fillText('frame ' + i, 100 + i * 20, 300);
const f = new VideoFrame(cv, { timestamp: i * 33333 });
enc.encode(f, { keyFrame: i === 0 }); f.close();
}
await enc.flush(); enc.close();
return { codec, hint, supported, bytes, ms: Math.round(performance.now() - start) };
}
| Build | HEVC Main | HEVC Main10 | What happened when I encoded |
|---|---|---|---|
| Chromium 149 / Chrome 155 | true for all three hints | false | prefer-hardware: 165–175 ms, 69,294 bytes. prefer-software: 6.0–6.5 s, 103,469 bytes |
| WebKit 26.5 (macOS) | true for all three hints | true | Every hint gave the same bytes and time. Main10 output was identical to Main, 67,581 bytes |
| Firefox 151 | false | false | Nothing to encode |
| Chromium 149, hardware video encode off | false | false | All three hints flip to false |
| Stripped-down Chromium 149 | false | false | Nothing to encode |
Different bytes meant a different encoder
In Chromium and Chrome I expected prefer-software to be false, because I had assumed Chromium only does HEVC through hardware. It came back true, and the encoder really ran. The byte count gave it away before the clock did: the same 30 frames came out as 103,469 bytes instead of 69,294, so something else had done the encoding. Then the clock agreed, 6.0–6.5 seconds instead of 165–175 ms. At that pace a full one-minute clip would take minutes, which is not something I'd offer as a default. The part I still don't understand is what happens with hardware encoding off. prefer-software went false as well. My guess is that the software path is a system encoder from macOS rather than something inside Chromium, but I haven't verified that, and I can't explain why turning off hardware encoding takes it down too.
Identical bytes meant the hint was ignored
WebKit was the opposite problem. It returned true for every HEVC config, Main10 included. The three hints produced byte-for-byte the same output at the same speed, so the hint is simply ignored there. Main10 produced exactly the same 67,581 bytes as Main from the same frames. I didn't parse the bitstream, so I can't prove what profile it really wrote, but I'm not going to tell anyone that WebKit encodes Main10 based on that true. A false wasn't simpler either: Firefox 151 said no to every HEVC config and still played my H.265 files in a <video> element, and the stripped-down Chromium's no describes that build, not the Chrome a classmate would open.
I also wanted to see whether a shipped tool reads these answers the same way, so I used ImgIng (https://imging.ai/), whose compressor encodes video in the browser, in the two Chromium setups. With hardware encoding on, its capability panel listed H.265 output as native hardware. With hardware encoding off, it listed H.265 output as unavailable for an H.264 source, and moved an H.265 source to a local WebAssembly path. So it keeps the fast answer and the fallback apart instead of trusting a single yes. How it decides internally I can't see.
How I read a true now
I keep a small table per build. For each HEVC config I encode the same 30 frames under all three hints and write down bytes and milliseconds. Two hints with identical bytes went through the same encoder, so the hint did nothing. Different bytes mean the encoder changed, and the time tells me which one I'd actually be shipping. Identical bytes for Main and Main10 make me distrust the Main10 answer. On this Mac that leaves HEVC Main under prefer-hardware in Chromium and Chrome as the answer I believe, plus a WebKit true that is fast but says nothing about the profile. Playback gets its own check with canPlayType, because none of the encode answers say anything about it.
All of this comes from one M4 running macOS 26.5. Windows depends on the GPU and system components, and I haven't touched Windows, Linux, Android or Safari on an iPhone, so none of these numbers transfer. The bytes also only tell me the encoder changed. I never scored those 30 frames, so I can't say which output looks better. In my project H.265 stays behind a checkbox marked experimental until I've filled in the same table on one of the Windows machines in the library.
Top comments (0)