← All articles
ffmpeg Loses HEVC Alpha: Transparent Video Fix
Transparent HEVC .mov comes out of ffmpeg with a baked-in background? ffmpeg silently drops Apple's alpha layer. The ProRes 4444 pipeline that fixes it.

A designer sends over a logo animation exported as "HEVC with alpha" — transparent background, drop it on any colour, done. You run it through ffmpeg to make a web-friendly mp4 and a webm, and both come back with a solid white or black background. Extract a still and it looks corrupted: tan patches, stray shapes floating inside white circles. You check ffprobe, it says pix_fmt=yuv420p, and you go back to the designer to tell them they forgot the alpha.
They didn't. ffmpeg HEVC alpha transparency is lost at decode time, silently, and ffprobe is not able to tell you. This is the full debug trail from a real client build, plus the pipeline that actually produces a transparent video for the web in both Safari and Chrome.
The symptom: a "transparent" clip that isn't
The source is a QuickTime .mov exported from After Effects or Motion with the Apple HEVC with Alpha codec. Everything about it looks right in QuickTime Player and in Finder's preview — the background is the checkerboard.
Then:
ffmpeg -i in.mov -c:v libvpx-vp9 -pix_fmt yuva420p out.webm
ffmpeg -i in.mov -vf "select=eq(n\,0)" -vframes 1 frame.png
The webm plays with a hard background. frame.png shows the artwork sitting on garbage — blotches of colour that were never in the design. No warning, no error, exit code 0.
Two false leads worth skipping:
-hwaccel videotoolboxdoes not help. Hardware acceleration swaps the pixel decoder; layer selection stays with ffmpeg's demuxer, which is where the problem is.- No encode flag can fix it.
-pix_fmt yuva420p,-auto-alt-ref 0,-alpha_quality— all of them operate on frames that already lost their alpha. You cannot re-encode information that was discarded during decode.
Why ffprobe reports yuv420p and is still wrong
Apple's HEVC-with-alpha is two layers inside one stream: an opaque full-frame base layer, plus an auxiliary alpha layer, tagged with HEVC syntax that identifies the pair. Apple documents the arrangement in the HEVC Video with Alpha Interoperability Profile and in Using HEVC video with alpha.
ffmpeg decodes only the base layer and discards the auxiliary layer without a warning. ffprobe reads the same base layer, so it reports pix_fmt=yuv420p — which is a true description of what ffmpeg can see and a false description of the file.
And the "corruption" in the extracted stills is not corruption at all. It is legitimate base-layer RGB in the regions where alpha is 0. The encoder is free to leave anything under a fully-masked area, because that data was never meant to be looked at. Once you strip the mask, you are looking at the encoder's scratch paper.
So the first rule: ffprobe pix_fmt=yuv420p does not prove a .mov is opaque.
Detect alpha with AVFoundation, not ffprobe
The source of truth on macOS is CoreMedia's format description. Save this as alpha-check.swift and run it with swift alpha-check.swift in.mov (top-level await needs Swift 5.7 / Xcode 14+):
import AVFoundation
import CoreMedia
let url = URL(fileURLWithPath: CommandLine.arguments[1])
let asset = AVURLAsset(url: url)
guard let track = try await asset.loadTracks(withMediaType: .video).first else {
print("no video track"); exit(1)
}
for desc in try await track.load(.formatDescriptions) {
let alpha = CMFormatDescriptionGetExtension(
desc,
extensionKey: kCMFormatDescriptionExtension_ContainsAlphaChannel
)
print("ContainsAlphaChannel:", alpha ?? "absent")
}
1 means the track declares an alpha channel. Run this on every "transparent" clip a designer hands you before you blame the export — in my case it printed 1 on a file ffprobe had already convinced me was opaque.
Keep the caveat in mind though: this reads a format-description extension. It tells you what the file claims. The next section is why that distinction matters.
-c copy does not get you out of this
The obvious escape hatch is to not decode at all — just rewrap the designer's stream into an mp4 so Safari can play it:
ffmpeg -i in.mov -c copy -tag:v hvc1 out.mp4
The output is byte-for-byte nearly the same size as the input. The alpha payload is still physically in there. And it does not play:
AVAssetReaderfails to start with-11801/OSStatus -12713.- Safari refuses the file outright.
The reason is that ffmpeg rewrites the hvcC configuration box from the parameter sets it understands — base layer only — and drops the declaration the VideoToolbox decoder needs to find the second layer. Remuxing .mov → .mov fails identically, so the container brand (qt vs isom) is not the variable.
Two options remain: re-encode through a format that carries alpha in the clear, or ship the designer's original file untouched. If you go the second route, check that moov sits before mdat first — Apple exports usually already are faststart, so there is often nothing to "fix" and therefore no reason to run ffmpeg over it at all.
The alpha-preserving pipeline
The trick is to decode with AVFoundation (which reads both layers) and hand ffmpeg a format it can actually see the alpha in. avconvert ships with macOS.
Step 1 — ProRes 4444 intermediate
avconvert --preset PresetAppleProRes4444LPCM --source in.mov --output prores.mov
Verify it worked before you go any further:
ffprobe -v error -select_streams v:0 -show_entries stream=pix_fmt -of csv=p=0 prores.mov
# yuva444p12le
That yuva444p12le is the whole point — the a is alpha ffmpeg can now see. The intermediate is big (expect several hundred MB for a few seconds of 4K); it is a scratch file, delete it after.
Step 2 — VP9 with alpha for Chrome, Firefox, Edge
ffmpeg -i prores.mov -c:v libvpx-vp9 -pix_fmt yuva420p -crf 30 -b:v 0 \
-g 1 -an -row-mt 1 -auto-alt-ref 0 out.webm
-auto-alt-ref 0 is required. VP9's alternate-reference frames are incompatible with the alpha side-stream; leave it on and the alpha comes out broken or missing, again without an error. -g 1 forces every frame to be a keyframe — only needed if the clip is scroll-scrubbed, for the same reason described in why scroll-scrubbed video stutters. Drop it for a normal autoplay loop and the file gets much smaller.
Step 3 — HEVC with alpha for Safari
ffmpeg -i prores.mov -c:v hevc_videotoolbox -allow_sw 1 -alpha_quality 0.9 \
-tag:v hvc1 -g 1 -an -movflags +faststart out.mp4
hevc_videotoolbox is the macOS hardware encoder — this step only runs on a Mac. -allow_sw 1 lets it fall back to software on machines without an HEVC encode block. -tag:v hvc1 is mandatory: with the default hev1 tag Safari will not play the file. -alpha_quality 0.9 controls the alpha layer's bitrate independently of the base layer.
If ffmpeg on your Mac throws Operation not permitted on any of these while reading from ~/Downloads or ~/Desktop, that is a separate macOS problem with a separate fix — see ffmpeg "Operation not permitted" on macOS.
Markup: don't trap Chrome on the hvc1 file
Order matters, and so does the codecs string:
<video autoplay muted loop playsinline preload="metadata">
<source src="/media/logo.mp4" type='video/mp4; codecs="hvc1"' />
<source src="/media/logo.webm" type="video/webm" />
</video>
The mp4 goes first so Safari picks it up. Chrome cannot decode hvc1 in most configurations — the explicit codecs="hvc1" string is what lets it reject the source and fall through to the WebM. Write a bare type="video/mp4" instead and Chrome accepts the source, then fails to decode it, and you get a black box with no console error.
Nothing about transparency exempts you from the usual media budget: a full-bleed alpha loop is still bytes on the critical path, still competes with your hero animation, and still shows up in Core Web Vitals on animation-heavy sites. Give it preload="metadata" and gate it behind the fold when you can.
Verify by decoding a frame, never by metadata
This is the lesson that cost me the most time. The ContainsAlphaChannel check from earlier still returns 1 on the broken -c copy remux — the one VideoToolbox refuses to open. It is a format-description extension, not proof the payload is readable.
The only honest verification is decoding an actual frame and reading the alpha byte:
let reader = try AVAssetReader(asset: asset)
let output = AVAssetReaderTrackOutput(track: track, outputSettings: [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
])
reader.add(output)
reader.startReading()
guard let sample = output.copyNextSampleBuffer(),
let pixels = CMSampleBufferGetImageBuffer(sample) else {
print("no frame decoded — reader status \(reader.status.rawValue): \(String(describing: reader.error))")
exit(1)
}
CVPixelBufferLockBaseAddress(pixels, .readOnly)
let base = CVPixelBufferGetBaseAddress(pixels)!.assumingMemoryBound(to: UInt8.self)
let rowBytes = CVPixelBufferGetBytesPerRow(pixels)
let w = CVPixelBufferGetWidth(pixels)
let h = CVPixelBufferGetHeight(pixels)
var lo: UInt8 = 255
var hi: UInt8 = 0
for y in 0..<h {
for x in 0..<w {
let a = base[y * rowBytes + x * 4 + 3]
lo = min(lo, a)
hi = max(hi, a)
}
}
CVPixelBufferUnlockBaseAddress(pixels, .readOnly)
print("alpha min=\(lo) max=\(hi)")
A good file prints alpha min=0 max=255 — fully transparent pixels and fully opaque ones both present. A broken file never decodes a frame at all; it exits on the guard with -11801 / OSStatus -12713.
For the WebM output, ffprobe is finally useful, because VP9 alpha lives in a container-level flag:
ffprobe -v error -select_streams v:0 -show_entries stream_tags=alpha_mode -of csv=p=0 out.webm
# 1
alpha_mode is the WebM container's AlphaMode element, documented in the WebM container guidelines. And the last check, in the browser that will actually serve it:
const v = document.querySelector('video');
await v.play();
const c = document.createElement('canvas');
c.width = v.videoWidth;
c.height = v.videoHeight;
const ctx = c.getContext('2d', { willReadFrequently: true });
ctx.drawImage(v, 0, 0);
const [r, g, b, a] = ctx.getImageData(2, 2, 1, 1).data;
console.log({ r, g, b, a }); // a === 0 in a corner that should be transparent
What each check is actually worth
| Check | What it reports | Verdict |
|---|---|---|
ffprobe pix_fmt on the HEVC source | yuv420p even when alpha exists | Blind — never use it here |
ContainsAlphaChannel extension | 1 on the source and on the broken remux | Necessary, not sufficient |
AVAssetReader + 32BGRA frame read | alpha min=0 max=255, or no frame at all | The only real proof for mp4 |
ffprobe stream_tags=alpha_mode on webm | 1 when VP9 alpha survived | Trustworthy for WebM only |
Canvas getImageData corner alpha | 0 on a transparent corner | Trustworthy — the browser's own decoder |
Note the asymmetry: ffprobe stays blind to hvc1 alpha even on your own output from step 3. Do not treat a yuv420p reading on out.mp4 as a failure — run the AVFoundation frame read instead.
FAQ
Why does ffmpeg say yuv420p on a video with an alpha channel?
Because Apple HEVC-with-alpha stores alpha in a separate auxiliary layer. ffmpeg decodes and probes only the base layer, which genuinely is yuv420p. The alpha is in the file; ffmpeg simply does not surface it.
Can I convert HEVC with alpha to a transparent WebM in one ffmpeg command?
Not from the original .mov, no — the alpha is gone before any filter or encoder in that command runs. You need an AVFoundation-based decode first, which in practice means the avconvert ProRes 4444 intermediate, then libvpx-vp9 with -pix_fmt yuva420p -auto-alt-ref 0.
Does -c copy preserve the alpha?
The payload survives the copy, but the resulting file will not decode: AVAssetReader returns -11801 / OSStatus -12713 and Safari refuses to play it, because ffmpeg rewrites hvcC from the base-layer parameter sets only. Ship the original file untouched instead, or re-encode via ProRes.
Do I still need the WebM if Safari plays the hvc1 mp4?
Yes. Chrome, Firefox and Edge cannot be relied on to decode hvc1, so the VP9-alpha WebM is the fallback for the majority of traffic. Two files is the current cost of transparent video on the open web.
Is transparent video worth it over a PNG sequence or Lottie?
For vector-ish motion, Lottie is smaller and sharper — use it. Transparent video earns its place when the clip has real texture, grain, blur, or filmed footage that vector formats cannot express. A PNG sequence is almost never the right answer at web scale.
Getting media like this to survive the trip from the design tool to the browser is the unglamorous half of building an award-level site — the half that decides whether the idea ships at all. If you have a build where the motion is right but the pipeline keeps eating it, let's talk about the project.


