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.
An MP4 is a pile of boxes. Two of them matter:
mdatis the payload: every frame and every audio sample, back to back.moovis 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.
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.
# 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 -ExplainIf PowerShell refuses to run it:
powershell -NoProfile -ExecutionPolicy Bypass -File .\muxcheck.ps1 .\replay.mp4| 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.
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 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| 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" }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.
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.ps1builds MP4 files byte by byte withmp4gen.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-Explainprints 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.ps1runs 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.ps1is 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 purposeWindows 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.
| 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 |
- 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
MIT. See LICENSE.