Handling Corrupt MP4 Files in Automated Pipelines

Within the media validation and error routing stage of the broader Media Ingestion & Format Architecture pipeline, corrupt MP4 ingestion is the failure that most often slips past a naive gate and detonates downstream — this page covers the exact production scenario where a truncated upload reaches a GPU transcoder and starves the queue. Resolving it requires a strict pre-flight validation gate, precise probe threshold tuning, and explicit routing logic that quarantines recoverable media instead of retrying it into an infinite loop.

Problem Framing

Corrupt MP4 ingestion is a deterministic pipeline breaker that manifests as silent truncation, malformed container atoms, or bitstream discontinuities — and it rarely presents as a single failure vector. The most frequent diagnostic signatures are a missing or displaced moov atom, which prevents stream indexing and forces a linear mdat scan; a truncated ftyp header, which causes immediate parser rejection; and fragmented MP4 structures with misaligned sidx boxes, which trigger seek failures during random access. Audio streams frequently exhibit sample-rate discontinuities or dropped AAC/Opus frames that desynchronize during codec normalization, while video bitstreams contain invalid H.264/HEVC NAL unit boundaries — particularly when a network transfer is interrupted mid-write or camera firmware writes an incomplete SEI message. All of these conform to (or violate) the ISO/IEC 14496-12 base media file format.

The symptom that brings engineers to this page is an ingestion worker that hangs rather than fails. Default ffprobe behavior aggressively scans an entire file to reconstruct stream metadata, so a truncated input produces a worker that blocks for minutes and a queue that backs up behind it:

ERROR: worker-07 probe stalled 184s on s3://ingest/raw/ep-4412.mp4
[mov,mp4,m4a,3gp,3g2,mj2 @ 0x55f1] moov atom not found
[mov,mp4,m4a,3gp,3g2,mj2 @ 0x55f1] error reading header
batch_queue_depth{stage="ingest"} 5120  (rising)

Once the probe is unbounded, one bad asset is enough to saturate a worker pool. The fix is to treat container validation as a threshold-bounded checkpoint, not a reactive error handler.

Corrupt MP4 routing through a bounded ffprobe gate An MP4 upload arriving from an S3 or GCS event enters a CPU-only ffprobe gate bounded by probesize 50M, analyzeduration 30 seconds, and a hard timeout wall of about 5 seconds. The gate emits one deterministic verdict per asset and fans out to four isolated queues. A probe that times out or exits non-zero routes to dead_letter for alerting and backoff. A non-ISO container or zero duration routes to quarantine for forensic analysis. Audio sample-rate drift greater than 50 hertz routes to manual_review. Only an asset that clears every invariant passes to transcode_queue for GPU encoding. timeout / exit≠0 non-ISO · 0s sr drift >50 Hz all invariants pass MP4 upload S3 / GCS event Bounded ffprobe gate CPU-only · one verdict -probesize 50M -analyzeduration 30s timeout wall ~5s dead_letter probe failed · alert + backoff quarantine bad container · forensic replay manual_review sample-rate drift · human check transcode_queue container integrity guaranteed

Solution Architecture

The gate is a single CPU-only step that probes the asset under a hard timeout and emits a deterministic routing verdict. Its design follows three rules: cap probe cost so corrupt input fails fast, classify each defect into a distinct routing class, and never hand a malformed container to a downstream worker. ffprobe is constrained to a deterministic byte window with explicit error tolerance — -probesize 50M and -analyzeduration 30000000 (30 seconds, in microseconds) cap memory and CPU, while -err_detect ignore_err suppresses non-fatal bitstream warnings that would otherwise abort the probe. Layering an OS-level timeout on top of these bounds guarantees the worker returns within ~200 ms for a clean file and within a couple of seconds for a severely truncated one, establishing a hard ceiling for the ingestion worker. The parameter precedence and stream-mapping behavior are documented in the upstream FFprobe reference.

Each verdict maps to a queue. Probe-level failures (timeout, non-zero exit, unparseable JSON) go to dead_letter for alerting and backoff; structural defects (non-ISO container, zero duration) go to quarantine for forensic analysis; soft anomalies such as audio sample-rate drift go to manual_review; and only assets that clear every invariant reach transcode_queue. Files that pass here are exactly the inputs that the fallback routing for failed transcodes layer can then safely run through GPU encoders, because container integrity is already guaranteed.

Implementation

The validation routine parses the ffprobe JSON payload and cross-references container metadata against structural invariants — recognizable container format, consistent stream duration, and bounded audio sample-rate drift — before returning a routing decision. Every threshold is explicit so staging and production behave identically.

# Python 3.10+
# ffprobe from FFmpeg >= 6.0 must be on PATH
import subprocess
import json
import os
import logging
from typing import Dict, Any
from dataclasses import dataclass

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")


@dataclass
class ValidationResult:
    is_valid: bool
    routing_queue: str          # dead_letter | quarantine | manual_review | transcode_queue
    diagnostics: Dict[str, Any]


def probe_mp4(filepath: str, timeout: int = 5) -> Dict[str, Any]:
    """Execute ffprobe with constrained thresholds and return parsed JSON.

    -probesize / -analyzeduration cap how much the probe reads before it gives
    up reconstructing metadata; the subprocess `timeout` is the hard wall that
    stops a truncated moov-less file from hanging the worker.
    """
    cmd = [
        "ffprobe",
        "-v", "error",                 # structured errors only, no info noise
        "-show_format",
        "-show_streams",
        "-print_format", "json",
        "-probesize", "50M",           # cap bytes scanned for stream discovery
        "-analyzeduration", "30000000",  # 30s in microseconds
        "-err_detect", "ignore_err",   # tolerate non-fatal bitstream warnings
        filepath,
    ]
    try:
        result = subprocess.run(
            cmd, capture_output=True, text=True, check=True, timeout=timeout
        )
        return json.loads(result.stdout)
    except subprocess.TimeoutExpired:
        raise RuntimeError(f"Probe timed out after {timeout}s: {filepath}")
    except subprocess.CalledProcessError as e:
        raise RuntimeError(f"ffprobe exited {e.returncode}: {e.stderr.strip()}")
    except json.JSONDecodeError as e:
        raise RuntimeError(f"Invalid JSON payload from ffprobe: {e}")


def validate_container(filepath: str) -> ValidationResult:
    """Parse probe output, enforce structural invariants, and route accordingly."""
    try:
        probe_data = probe_mp4(filepath)
    except RuntimeError as e:
        # Probe could not complete at all -> fatal, alert + backoff.
        logging.error("Probe failure for %s: %s", filepath, e)
        return ValidationResult(
            is_valid=False,
            routing_queue="dead_letter",
            diagnostics={"error": str(e), "file": os.path.basename(filepath)},
        )

    fmt = probe_data.get("format", {})
    streams = probe_data.get("streams", [])
    diagnostics = {"file": os.path.basename(filepath), "format_tags": fmt.get("tags", {})}

    # 1. Confirm an ISO Base Media container. A successful return of format
    #    metadata implies ffprobe located the moov atom; major_brand carries
    #    values such as "isom", "mp42", "M4V ", "qt  ".
    format_name = fmt.get("format_name", "")
    major_brand = fmt.get("tags", {}).get("major_brand", "").strip().lower()
    iso_brands = {"isom", "mp41", "mp42", "m4v", "m4a", "qt",
                  "iso2", "iso4", "iso5", "iso6"}
    if (not any(b in format_name for b in ("mp4", "mov", "m4a"))
            or (major_brand and major_brand not in iso_brands)):
        diagnostics["failure_reason"] = "Missing or malformed ISO Base Media container"
        return ValidationResult(False, "quarantine", diagnostics)

    # 2. Validate duration consistency. A <=0.1s duration means the container
    #    was truncated before the mdat carried real samples.
    try:
        duration_sec = float(fmt.get("duration", 0))
        if duration_sec <= 0.1:
            diagnostics["failure_reason"] = "Truncated or zero-duration container"
            return ValidationResult(False, "quarantine", diagnostics)
    except (ValueError, TypeError):
        diagnostics["failure_reason"] = "Unparseable duration metadata"
        return ValidationResult(False, "quarantine", diagnostics)

    # 3. Detect audio sample-rate drift across streams. >50 Hz delta between
    #    audio streams desyncs during codec normalization -> human review.
    audio_streams = [s for s in streams if s.get("codec_type") == "audio"]
    if audio_streams:
        base_sr = int(audio_streams[0].get("sample_rate", 0))
        drift = any(
            abs(int(s.get("sample_rate", 0)) - base_sr) > 50 for s in audio_streams
        )
        if drift:
            diagnostics["failure_reason"] = "Audio sample rate drift detected"
            return ValidationResult(False, "manual_review", diagnostics)

    diagnostics["status"] = "passed"
    return ValidationResult(True, "transcode_queue", diagnostics)


if __name__ == "__main__":
    import sys
    target_file = sys.argv[1] if len(sys.argv) > 1 else "input.mp4"
    result = validate_container(target_file)
    print(json.dumps(result.__dict__, indent=2))

Wire this routine in at the S3/GCS event trigger or the message-queue consumer, so no raw upload can bypass it. The quarantine and manual_review classes share the dead-letter machinery — visibility timeouts, poison-message detection, and replay tooling — that the retry logic and dead-letter queue patterns define once for the whole platform.

Verification

Confirm the gate behaves on three classes of input: a clean file, a header-truncated file, and a moov-less file. Synthesize the corrupt cases directly so the test corpus is reproducible:

# Clean reference asset
ffmpeg -f lavfi -i testsrc=duration=5:size=320x240:rate=30 \
       -f lavfi -i sine=frequency=440:duration=5 \
       -c:v libx264 -c:a aac clean.mp4

# Header-truncated asset (drops the ftyp/moov region)
tail -c +4096 clean.mp4 > truncated.mp4

# Run the gate
python validate_mp4.py truncated.mp4

A truncated input must route to quarantine (or dead_letter if the probe cannot return at all), never to transcode_queue:

{
  "is_valid": false,
  "routing_queue": "quarantine",
  "diagnostics": {
    "file": "truncated.mp4",
    "failure_reason": "Missing or malformed ISO Base Media container"
  }
}

Independently confirm what ffprobe itself reports, and assert the gate honors its time budget:

# moov-less files print this on stderr and exit non-zero
ffprobe -v error -show_format truncated.mp4
# -> [mov,mp4,m4a,...] moov atom not found

# The probe must finish well under its wall-clock ceiling, even on bad input
/usr/bin/time -v python validate_mp4.py truncated.mp4 2>&1 | grep "Elapsed"

In production, assert the routing invariant as a metric rather than a log scrape: quarantine_total and dead_letter_total should rise on bad batches while transcode_jobs_total only ever counts assets that passed, and ingest_probe_seconds (a histogram) should stay flat regardless of how corrupt the input cohort is.

Failure Modes & Edge Cases

Edge case Probe signature Correct route Note
Fragmented MP4 with moov at EOF probe slow but succeeds within budget transcode_queue streaming-optimized; do not quarantine valid fMP4
moov atom never written moov atom not found, non-zero exit quarantine classic interrupted upload
Header bytes truncated format_name empty / parse error quarantine ftyp lost; container unrecognizable
Zero / sub-0.1s duration format.duration <= 0.1 quarantine mdat carries no real samples
44.1 kHz + 48 kHz audio streams sample-rate delta > 50 Hz manual_review desyncs during normalization, not fatal
Valid container, broken NAL units probe passes, decode later fails transcode_queue then DLQ demux-clean defects surface at decode
Probe exceeds hard timeout TimeoutExpired raised dead_letter alert + backoff; never silently retry
Variable frame rate source probe succeeds, vfr flagged transcode_queue downstream must force CFR, not a corruption

Two of these deserve emphasis. A broken NAL boundary inside an otherwise valid container will pass a metadata-only probe and only fail at decode time — the fallback routing for failed transcodes layer is what catches these, classifying the FFmpeg Invalid data found when processing input as deterministic and routing it straight to the dead-letter queue. And fragmented MP4 with a trailing moov is valid: keep probesize/analyzeduration generous enough that streaming-optimized assets are not misclassified as corrupt.

FAQ

Why route a corrupt MP4 to quarantine instead of retrying it?

Structural container corruption is deterministic, not transient — a missing moov atom is missing on every attempt, so retrying just burns the same cycles three times and can wedge the queue. Quarantine isolates the asset for forensic replay while keeping the worker pool moving; only probe-level transient faults (broker timeout, scratch-disk full) belong on a retry path.

How do I keep the probe from hanging on a truncated file?

Bound it twice. Set -probesize 50M and -analyzeduration 30000000 so ffprobe stops trying to reconstruct metadata after a fixed budget, and wrap the subprocess in a hard timeout (5s here) so even a pathological input cannot block the worker. The hard timeout is the guarantee; the probe flags just make the common case fast.

Does a successful probe mean the file will transcode cleanly?

No. The gate validates container structure and stream metadata, which proves the moov atom was located and the duration is sane, but it does not decode every frame. Defects like broken H.264 NAL boundaries surface only at decode time, which is why the downstream transcode worker still needs deterministic dead-letter handling for Invalid data found when processing input errors.