Skip to content

Repository files navigation

muxcheck

Tells you why an MP4 scrubs badly, uploads slowly, or will not play at all - without decoding a single frame.

A recording can be perfectly valid and still be miserable to work with. The bytes are fine, the codec is fine, and the file still takes forty seconds to open in an editor. The reason is almost never the video: it is where the recorder put the index, and how it wove the audio through the picture.

muxcheck reads the container layout and tells you which one you have got.

Windows PowerShell 5.1. No installs, no modules, no ffmpeg. One file.

The problem

An MP4 is a pile of boxes. Two of them matter:

  • mdat is the payload: every frame and every audio sample, back to back.
  • moov is the index: where each sample lives, how long it lasts, what track it belongs to.

A player cannot show frame one until it has read moov. If your recorder wrote moov at the end - and OBS, ShadowPlay and most capture cards do, because the index is not finished until recording stops - then the player has to reach the end of the file before it can start.

On a local SSD you never notice. You notice when you:

  • scrub in an editor and it stalls on every seek
  • upload a 40 GB session and the site spends an extra pass "processing"
  • try to play a clip straight off a network share
  • open a recording that a crash cut short, and nothing can read it

Every one of those is a layout problem, and none of them show up in a codec check. muxcheck names which one it is.

What it prints

moov-last.mp4
  15.8 KB, 10 s, 2 track(s)
  layout: ftyp mdat moov
  worst interleave gap: 2 KB (1.36 s of playback)

  warning: the index is at the end, so nothing can start until the whole file is read

With -Explain, every finding carries its code and the full reason:

bad-interleave.mp4
  816.8 KB, 10 s, 2 track(s)
  layout: ftyp moov mdat
  worst interleave gap: 768 KB (9.412 s of playback)

  note: the index is at the front, so playback can start immediately
      FASTSTART
      The moov box sits at byte 24, ahead of the media at byte 766. A player
      or a web browser can begin as soon as the first chunk arrives.
  warning: audio and video are woven badly: a 768 KB run with none of the other track
      POOR_INTERLEAVE
      The worst video to audio distance is 768 KB at byte 774, about 9.41 s of
      playback. A player must hold all of it before it can present that moment
      with sound, which is felt as a stall when scrubbing and as a long buffer
      when streaming.

And a recording that was cut short:

cut-short.mp4
  8.8 KB
  layout: ftyp mdat

  problem: the file is cut short: 'mdat' is missing 6.2 KB
      TRUNCATED_BOX
      The 'mdat' box at byte 24 declares 15368 bytes but only 8976 are
      present. The recording stopped before the muxer finished writing.
  problem: there is no index, so nothing can play this file
      NO_MOOV
      The moov box holds every sample position and timing. Without it a player
      sees only undifferentiated bytes. This is what a recorder leaves behind
      when it is killed mid-recording, because the index is written last.

Quick start

# one file
.\muxcheck.ps1 .\replay.mp4

# a whole folder of captures
.\muxcheck.ps1 D:\Recordings

# everything underneath, but only the files with something wrong
.\muxcheck.ps1 D:\Recordings -Recurse -Quiet

# why, in full
.\muxcheck.ps1 .\replay.mp4 -Explain

If PowerShell refuses to run it:

powershell -NoProfile -ExecutionPolicy Bypass -File .\muxcheck.ps1 .\replay.mp4

Options

Option What it does
-Explain also print the finding code and the full explanation
-Json machine readable output, one object per file
-Recurse with a folder, also look in subfolders
-Filter wildcard applied to file names, e.g. -Filter "replay*"
-Quiet only print files that have something wrong
-NoColor never colour the output
-Version print the version and exit
-Help print the help and exit

-Quiet suppresses the clean verdicts only. A file with a warning still prints, because a warning is the entire reason you ran the sweep.

Colour goes through the Win32 console API, not escape sequences, so redirected output is plain text whether or not you pass -NoColor.

What it checks

23 findings, at three severities. A note is information, a warning is something you would want to know about, and a problem means the file is broken.

Code Severity Means
TRUNCATED_BOX problem a box claims more bytes than the file holds
NO_MOOV problem no index, so nothing can play it
NO_MDAT problem an index with no media behind it
NO_TRACKS problem an index that describes nothing
CHUNK_PAST_EOF problem a chunk offset points past the end of the file
OFFSETS_TOO_SMALL problem 32 bit offsets in a file too big for them
MOOV_AFTER_MDAT warning the index is last, so nothing starts until the end
POOR_INTERLEAVE warning a long run of one track with none of the other
HUGE_INDEX warning over 4 MB of index to read before frame one
LARGE_PADDING warning over 1 MB of free/skip doing nothing
TAIL_BYTES warning bytes after the last box that belong to no box
MULTIPLE_MOOV warning more than one index; players disagree on which wins
FRAGMENTED warning a fragmented MP4, which many editors will not open
NO_FTYP warning no brand box, so the file type is a guess
NO_SAMPLES warning a track whose sample table is empty
TRACK_NO_CHUNKS warning a track with no chunk offsets
EDIT_LIST_TRIM warning an edit list skips the opening of a track
EDIT_LIST_DELAY warning an edit list holds a track back from zero
EDIT_LIST_COMPLEX warning an edit list too involved for most tools to honour
FASTSTART note the index is at the front; this is the good case
MDAT_SPLIT note media in more than one mdat
CO64_USED note 64 bit chunk offsets, expected on large files
SINGLE_TRACK note only one track, so there is nothing to interleave

Edit lists get three separate findings because they are the quietest way for a file to be "fine" and still land out of sync: an editor that honours the edit list and a tool that ignores it will disagree by exactly the trim.

JSON

-Json emits one object per file, suitable for piping into anything:

[
    {
        "file":  "C:\\Recordings\\moov-last.mp4",
        "name":  "moov-last.mp4",
        "sizeBytes":  16134,
        "seconds":  10,
        "boxOrder":  [ "ftyp", "mdat", "moov" ],
        "verdict":  "warning",
        "interleave":  {
                           "measured":  true,
                           "worstBytes":  2048,
                           "worstAt":  14880,
                           "worstSeconds":  1.3599,
                           "direction":  "audio to video"
                       },
        "tracks":  [
                       {
                           "trackId":  1,
                           "kind":  "vide",
                           "timescale":  600,
                           "seconds":  10,
                           "chunks":  6,
                           "offsetBox":  "stco",
                           "samples":  60,
                           "sampleBytes":  12000,
                           "editEntries":  0
                       }
                   ],
        "findings":  [
                         {
                             "code":  "MOOV_AFTER_MDAT",
                             "severity":  "warning",
                             "title":  "the index is at the end, so nothing can start until the whole file is read"
                         }
                     ]
    }
]

Find every recording in a library that needs a faststart pass:

$report = .\muxcheck.ps1 D:\Recordings -Recurse -Json | ConvertFrom-Json
@($report | Where-Object { $_.findings.code -contains 'MOOV_AFTER_MDAT' }).Count

Exit codes

Code Means
0 nothing at problem severity
1 at least one problem
2 nothing could be read
3 a bug in muxcheck

A warning exits 0 on purpose. Warnings are advice about a file that works; scripts should only stop for something actually broken.

.\muxcheck.ps1 D:\Recordings -Recurse -Quiet
if ($LASTEXITCODE -eq 1) { "at least one recording is damaged" }

It does not read the whole file

muxcheck seeks the top level box headers, then reads only the moov box. It never touches mdat, never decodes a frame and never opens the bitstream.

A 37.3 MB recording is answered in 1.87 s, and almost all of that is the index. A 40 GB session costs about the same, because the payload is skipped rather than read.

That is also the limit of what it can tell you. muxcheck knows where your samples are and how big they are. It does not know what is in them, so it will never comment on your bitrate, your colour tags or your encoder settings.

How it is tested

The layout bugs this tool looks for are all off-by-one errors on big endian integers read out of a binary file, which is the easiest kind of code to get quietly wrong. So the tests are the point.

  • selftest.ps1 builds MP4 files byte by byte with mp4gen.ps1, covering every finding above and the malformed files that should not crash the parser. 568 assertions across 34 groups. Two of those groups are censuses: one fails the suite if any finding code is never asserted on, the other fails it if any finding's severity is never pinned. A third pins the computed numbers inside the explanation sentences, which -Explain prints and the JSON does not carry - the one surface where a wrong number is invisible to every other assertion. Asserting that a finding fires is not the same as asserting how loudly it fires, or that the byte offset it quotes is the right one.
  • realcheck.ps1 runs against the actual recordings on the machine and checks the answers against two independent sources of truth: a separately written box walker, and the duration Windows Explorer gets from its own demuxer. 162 assertions across 13 groups. It also copies real files and damages them on purpose - truncated, index removed, bytes flipped - to check the failure paths on real data rather than on fixtures.
  • mutate.ps1 is the honest part. A green suite proves nothing by itself, so this breaks muxcheck in 197 specific ways, one at a time, and checks the tests notice. Six of those 197 are tripwires: changes that alter nothing a user could observe. A suite that "catches" a tripwire is asserting on noise, and the harness fails itself.
.\selftest.ps1     # hermetic, builds its own fixtures
.\realcheck.ps1    # needs real recordings on the machine
.\mutate.ps1       # slow, breaks the tool on purpose

Requirements

Windows PowerShell 5.1, which ships with Windows 10 and 11. Nothing else.

No modules, no ffmpeg, no .NET SDK, no admin rights. It never writes to the files it reads, and it makes no network calls.

Files

File What it is
muxcheck.ps1 the tool
selftest.ps1 hermetic test suite
realcheck.ps1 test suite against real recordings
mp4gen.ps1 builds MP4 files byte by byte for the tests
mutate.ps1 mutation harness
muts.txt the 194 mutations, in a readable diff format
genmuts.ps1 compiles muts.txt into mutate.ps1
greenrounds.ps1 runs the harness until it has three clean rounds
makedemo.ps1 builds the example files shown above

See also

  • obs-4k60-recorder - the settings that make the recording in the first place
  • framecheck - dropped and duplicated frames in a capture
  • truefps - what frame rate a recording actually runs at, from its own timing table
  • colorcheck - washed out or oversaturated recordings, from the colour tags

License

MIT. See LICENSE.

About

Explains why an MP4 or MOV scrubs badly, uploads slowly or will not play until fully downloaded. Reads the container layout directly: where the moov index sits relative to the mdat, how well audio and video are interleaved, wasted free padding, edit lists that shift the start, and recordings OBS never finalised. Zero dependencies, read-only.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages