Four media bugs worth understanding
A postMessage transfer list doesn't copy what it hands over. It detaches the buffer from every other reference to it, for good. That's one of four rules about media programming in the browser that are easy to break without noticing. Each one is worth knowing on its own, and each has a worked example in BeemMeUp's tools.
A transferred buffer is gone from where it came
Worker.postMessage() has two ways to send binary data. The default is a structured clone: the browser copies the bytes and sends the copy, safe but slow for anything large, like a video file. The second way is the transfer list, a second argument that names which ArrayBuffers to hand over instead of copy. A transfer is nearly free, because there's no copy, but "hand over" is literal. The sending side's buffer is detached the instant the message goes out. Its byteLength drops to zero. Read it again and you get nothing.
That's fine when the sender is done with the data. It's a bug the moment anything else still holds a reference to the same buffer.
Writing an input file into an FFmpeg worker is a common place for this. If the file's bytes are also kept elsewhere, say for the next rung of an encode ladder, transferring them leaves that other holder with an empty buffer, and the next encode reads nothing. Studio transfers a copy instead:
// apps/web/lib/ffmpeg/studio/runtime.ts
export function writeFilePayload(data: FFFileData): {
payload: FFFileData;
transfer: Transferable[];
} {
if (!(data instanceof Uint8Array)) {
return { payload: data, transfer: [] };
}
const copy = data.slice();
return { payload: copy, transfer: [toTransferable(copy)] };
}
Only the copy gets detached. The caller's bytes survive the call, which is what every other holder of that reference already assumed.
A hdlr atom depends on where it sits in the tree
An MP4 or QuickTime file is a tree of boxes, called atoms in the QuickTime spec: moov contains trak, trak contains mdia, and so on down. A box's meaning comes from its four-character type and its position in that tree together. The hdlr atom shows why. One sits directly under mdia and names the track's real media type, video or audio. QuickTime also nests a second hdlr deeper, under mdia/minf, and that one names the handler for the track's data references. That's a detail of how the container locates its data, and says nothing about whether the track is video or audio.
A parser that matches on box type alone, without tracking depth, will find both and can let the second overwrite the first. Every QuickTime track then reads as generic data, with no codec and no frame count. BeemMeUp's analysis tool reads hdlr only where it sits directly under mdia.
How much FFmpeg tolerates is a setting
A damaged file doesn't have to stop a decode. By default FFmpeg's decoders try to conceal errors and keep going, and -err_detect moves that line in either direction. Its flags make detection stricter: crccheck verifies embedded checksums, bitstream and buffer flag bitstream and length problems, and explode aborts on even a minor error. careful, compliant and aggressive widen what counts as an error at all. ignore_err goes the other way and tells FFmpeg to push through errors it would otherwise stop on.
ffmpeg -err_detect ignore_err -i damaged.mp4 -c copy recovered.mp4
-err_detect ignore_err: placed before-i, so it applies to readingdamaged.mp4-c copy: rewrite the streams into a fresh container without re-encoding
For recovering a damaged file that's usually the trade you want: as much of the file back as possible, even if some frames come out wrong. A command generator has to keep options like this one intact. Studio carries flags that aren't in its filter catalog through a generic raw-option node, so the corruption-repair presets keep -err_detect ignore_err when they turn into a command.
Stats that update but a UI that doesn't
RTCPeerConnection.getStats() returns a live snapshot: bitrate, packet loss, the simulcast layers currently active. None of that is useful if the UI reading it doesn't know a new snapshot arrived. React decides whether memoized values and effects need recomputing by comparing object identity. Mutate an object's fields in place and hand back the same reference, and anything memoized on that reference sees no change, even though every field inside it is different.
A WebRTC stats panel is where this shows up. The peer connection keeps producing fresh numbers, but if the engine stores them in one long-lived object and mutates it, every view memoized on that object stays on its first render. BeemMeUp's WebRTC and DASH labs depend on a counter that increments on each poll, so a new sample always reads as a change.
All four rules come from the same place: media data is large, so code shares it, reuses it and updates it in place, and each of those shortcuts has a condition attached. Knowing the condition is most of the work.