All posts
EngineeringFFmpegStreaming19 Sep 2026 · 3 min read

QA sweeps found something in every tool

JPJean Perez

The 14 September parity audit against the design prototype produced 60 findings. The four browser-driven QA sweeps after it (one per tool) found fewer things each. Each one also turned up a root cause serious enough to be logged as that sweep's blocker or urgent finding.

A buffer the HLS tool was already using

Every "Generate HLS" run failed partway through the encode ladder with a postMessage error saying an ArrayBuffer was detached. The cause was in writeFile(): it handed the worker's transfer list the caller's own buffer, the same Uint8Array the Studio's file store was holding onto for retries and other ladder rungs. A postMessage transfer list doesn't copy. It detaches the buffer for every other holder of that reference, for good. The store's copy of the file came back a zero-length buffer the moment the worker took it.

The fix copies before transferring:

// 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 now. The caller's bytes survive the call, which is what every other consumer of that file (retries, the other ladder rungs) already assumed.

A second, smaller bug turned up in the same sweep: the global Cmd+Enter keydown listener was torn down and re-attached on every state change, which meant the first click or keystroke after picking a file landed in the gap between the two and was silently dropped.

What The FFMPEG read the wrong handler atom

A .mov file has two hdlr atoms in its container: one directly under mdia, naming the real track type (video or audio), and one nested further under mdia/minf, naming the data handler (an internal detail of how QuickTime organizes its metadata). The probe parser let the second one overwrite the first, so every QuickTime track in the analysis tool showed up labeled "data," with a null-by-null header and no frame count, regardless of what the track actually was.

The second bug in that tool was worse. The ffprobe panel for a loaded file was fabricated: a static "Exit code 0" with no stream data behind it. It's now built from parsed data when there is some, and labelled "estimate" when there isn't.

Corruption Check dropped a flag the compiler didn't recognize

Opening a corruption fix in the Studio silently dropped -err_detect ignore_err, along with two other flags. The Studio's command compiler discards any parsed option node without a matching catalog entry, which is correct for most node types and wrong for exactly one: the generic "raw option" node, whose entire purpose is to carry flags that have no catalog entry by design. Three of five corruption-fix presets, two of them flagged critical, produced commands that could not actually run, while the Studio still displayed a "Ready" badge on them. A broken command, badged as ready.

One mutated object, sixteen WebRTC findings

The WebRTC lab's peer connection worked correctly the whole time. The UI never showed it, though. Its engine builds one long-lived options and stream state object and mutates it in place rather than replacing it, so every React hook memoized on that object's identity never recomputed, even as bitrate, packet loss, and simulcast layers changed underneath it live. Bitrate readouts, loss stats, the SDP panel and the log all stayed frozen at their first values.

The DASH lab's sidebar already had the right pattern: it depends on a poll counter instead of the mutated object's identity. The fix ports that into five WebRTC hooks. A sixth hook with the same bug turned up during the fix. It wasn't on the findings list.