Ffmpeg bitstream
Apply bitstream filters (-bsf) with ffmpeg to rewrite packet metadata without re-encoding: h264_mp4toannexb, hevc_mp4toannexb, aac_adtstoasc, extract_extradata, dump_extra, setts, noise, remove_extra, filter_units, mjpeg2jpeg, mpeg4_unpack_bframes. Use when the user asks to fix MP4-to-TS mux errors, convert H.264/HEVC between MP4 and Annex-B, fix AAC ADTS to ASC for MP4, repair a broken container, change PTS/DTS without re-encode, or extract raw codec extradata/SPS-PPS.From its SKILL.md
npx -y skills add damionrashford/media-os --skill ffmpeg-bitstreamAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 17 stars17 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
16.6 KB, as published. Nobody here has run it
Ffmpeg Bitstream
Context: $ARGUMENTS
Quick start
- MP4 H.264 → TS/HLS:
-bsf:v h264_mp4toannexb→ Step 3, recipe A - MP4 HEVC → TS:
-bsf:v hevc_mp4toannexb→ Step 3, recipe A - TS AAC → MP4:
-bsf:a aac_adtstoasc→ Step 3, recipe B - Old DivX/XviD packed B-frames:
-bsf:v mpeg4_unpack_bframes→ Step 3, recipe D - Rewrite H.264 level / VUI:
-bsf:v h264_metadata=...→ Step 3, recipe E - Strip SEI / AUD / filler NALs:
-bsf:v filter_units=remove_types=6→ Step 3, recipe F - Dump extradata into every keyframe (streaming recovery):
-bsf:v dump_extra=freq=k - Zero-base timestamps without re-encoding:
-bsf:v setts=ts=PTS-STARTPTS - Debug NAL/SPS/PPS structure:
-bsf:v trace_headers
When to use
- Container mux fails with
Malformed AAC bitstream detected/H.264 bitstream not in Annex-B format. - Need to remux MP4 ↔ TS / HLS / MKV with
-c copy(no re-encode, no quality loss). - Change H.264/HEVC level, profile, color primaries, or VUI flags without touching pixels.
- Split a stream into segments that need keyframe-carried SPS/PPS for mid-stream joins.
- Strip SEI/AUD NAL units a downstream decoder chokes on.
- Rewrite PTS/DTS on the packet level (e.g. zero-base timestamps, offset by N).
- Pull SPS/PPS out of a file for a hardware pipeline.
- Fix broken old MP4s with packed B-frames (DivX/XviD era).
- For re-encode workflows use
ffmpeg-transcode; for trimming useffmpeg-cut-concat; for pure container inspection useffmpeg-probe.
Step 1 — Identify the mux / codec mismatch
Probe first — bsf choice depends on codec + source container + target container:
ffprobe -v error -show_entries stream=index,codec_type,codec_name,profile -of json "$IN"
What to look at:
- Video
codec_name=h264→ pickh264_mp4toannexborh264_metadata. - Video
codec_name=hevc/h265→ pickhevc_mp4toannexborhevc_metadata. - Audio
codec_name=aaccoming frommpegts→ needaac_adtstoascfor MP4. - Source container =
.mp4/.mov(AVCC / length-prefixed NAL) vs.ts/.flv(Annex-B / start-code NAL) — this is the framing that bsf rewrites.
Container ↔ framing cheat-sheet (H.264 / HEVC):
| Source container | Framing | Target container | Required -bsf:v |
|---|---|---|---|
.mp4 / .mov | AVCC | .ts / HLS / .flv | h264_mp4toannexb / hevc_mp4toannexb |
.ts / .flv | Annex-B | .mp4 / .mov | none (ffmpeg converts back automatically) |
.mkv | either | .mp4 / .ts | usually none (mkv stores extradata out-of-band) |
Audio (AAC):
| Source container | Framing | Target container | Required -bsf:a |
|---|---|---|---|
.ts / .aac | ADTS | .mp4 / .mov | aac_adtstoasc |
.mp4 / .mkv | ASC | .ts / .flv | none (auto) |
Step 2 — Pick the right bsf
Conversion filters (change framing / headers):
h264_mp4toannexb— MP4 AVCC → Annex-B. Required for MP4 → TS/HLS/FLV with-c copy. No options needed.hevc_mp4toannexb— same, for H.265/HEVC.aac_adtstoasc— ADTS → MP4 AudioSpecificConfig. Required for TS AAC → MP4-c copy.mpeg4_unpack_bframes— fix old DivX/XviD MP4s that packed a B-frame inside the preceding P-frame. Transparent no-op on normal MP4.mjpeg2jpeg— split an MJPEG video stream into individual JPEG packets (use with-c copy -f image2to writeframe_%04d.jpg).
Metadata rewriters (change header fields only):
h264_metadata,hevc_metadata,av1_metadata— edit SPS/VPS/PPS/OBU fields:level,profile,tier,aud,sample_aspect_ratio,video_format,colour_primaries,transfer_characteristics,matrix_coefficients,chroma_sample_loc_type,tick_rate,crop_{left,right,top,bottom},fixed_frame_rate_flag,overscan_appropriate_flag.
Extradata manipulation:
extract_extradata— emit codec extradata (SPS/PPS) as side-data packets.dump_extra=freq=k— repeat extradata before every keyframe (makes segments independently decodable; costs a few bytes per IDR).remove_extra=freq=k— inverse — strip extradata from keyframes.
NAL-unit surgery:
filter_units=remove_types=6— drop NAL units by type (see Step 3 recipe F andreferences/filters.mdfor the type table).filter_units=pass_types=...— whitelist mode.filter_units=discard_flags=...— drop packets with a given flag.
Timing / debugging / fuzz:
setts=ts=.../pts=.../dts=...— expression-based PTS/DTS rewrite. Expression vocab:PTS,DTS,STARTPTS,STARTDTS,PREV_INPTS,PREV_OUTPTS,PREV_INDTS,PREV_OUTDTS,N(packet index),TB(timebase).trace_headers— pretty-print SPS/PPS/slice headers to stderr. Read-only.noise— random byte fuzzing for robustness testing.amount,drop,dropamount.
Step 3 — Apply with -c copy
-bsf:v and -bsf:a only fire when the stream is copied. Any -c:v libx264 /
-c:a aac re-encode rewrites extradata itself — the bsf is silently ignored.
Recipe A — MP4 H.264/HEVC → TS / HLS:
# H.264
ffmpeg -i in.mp4 -c copy -bsf:v h264_mp4toannexb out.ts
# HEVC
ffmpeg -i in.mp4 -c copy -bsf:v hevc_mp4toannexb out.ts
Modern ffmpeg auto-inserts these when muxing mpegts from mp4 — but passing the flag explicitly is always safe and portable across older builds.
Recipe B — TS (AAC) → MP4:
ffmpeg -i in.ts -c copy -bsf:a aac_adtstoasc out.mp4
Unlike the video bsf, ffmpeg does NOT always auto-insert aac_adtstoasc. Always
pass it when targeting MP4 from a TS/FLV/raw-AAC source.
Recipe C — HLS segment with keyframe-carried extradata (mid-segment joinable):
ffmpeg -i in.mp4 -c copy \
-bsf:v "h264_mp4toannexb,dump_extra=freq=k" \
-f mpegts segment.ts
Chain multiple bsf with a comma inside the same -bsf:v argument.
Recipe D — Fix packed B-frames in old DivX/XviD MP4s:
ffmpeg -i old.avi -c copy -bsf:v mpeg4_unpack_bframes -f mp4 fixed.mp4
Symptom you're fixing: player shows "warning: Invalid and inefficient vfw-avi packed B-frames detected" or every other frame is black.
Recipe E — Rewrite H.264 metadata (level, VUI, SAR) in place:
# Claim Level 4.1 so a hardware decoder that refuses 4.2 will accept it:
ffmpeg -i in.mp4 -c copy -bsf:v "h264_metadata=level=4.1" out.mp4
# Tag BT.709 color primaries + transfer + matrix (HD):
ffmpeg -i in.mp4 -c copy \
-bsf:v "h264_metadata=colour_primaries=1:transfer_characteristics=1:matrix_coefficients=1" \
tagged.mp4
# Fix square-pixel signalling (SAR 1:1):
ffmpeg -i in.mp4 -c copy -bsf:v "h264_metadata=sample_aspect_ratio=1/1" out.mp4
HEVC/AV1 equivalents: hevc_metadata=..., av1_metadata=.... Same field names.
Recipe F — Strip NAL unit types (SEI, AUD, filler):
# H.264: strip SEI (type 6) — fixes some broadcast players that choke on timecode SEI:
ffmpeg -i in.mp4 -c copy -bsf:v "filter_units=remove_types=6" out.mp4
# Strip SEI + AUD + filler:
ffmpeg -i in.mp4 -c copy -bsf:v "filter_units=remove_types=6|9|12" out.mp4
H.264 NAL types you care about: 5=IDR, 6=SEI, 7=SPS, 8=PPS, 9=AUD, 12=filler.
HEVC types live in a different range — see references/filters.md.
Recipe G — Zero-base timestamps:
# Reset PTS/DTS so the file starts at 0, preserving spacing:
ffmpeg -i in.mp4 -c copy \
-bsf:v "setts=ts=PTS-STARTPTS" \
-bsf:a "setts=ts=PTS-STARTPTS" \
zeroed.mp4
# Offset video by +2s (shift audio-video sync):
ffmpeg -i in.mp4 -c copy -bsf:v "setts=ts=PTS+2/TB" out.mp4
Remember: setts expressions run in the packet timebase — multiply seconds by
1/TB (or equivalently, divide by TB).
Recipe H — Extract SPS/PPS (extradata) as side-data:
ffmpeg -i in.mp4 -c:v copy -bsf:v extract_extradata -f null -
# Combined with ffprobe -show_packets -show_data to read the side-data bytes.
For most practical needs use ffprobe directly — extract_extradata is mostly a
building block for piping to another tool.
Recipe I — MJPEG video stream → individual JPEG files:
ffmpeg -i in.avi -c:v copy -bsf:v mjpeg2jpeg frame_%04d.jpg
Recipe J — Debug / inspect headers:
ffmpeg -i in.mp4 -c:v copy -bsf:v trace_headers -f null - 2>&1 | less
Dumps every SPS / PPS / slice header field in human-readable form. Needs a build with CBS (coded-bitstream) support — almost every modern ffmpeg has it.
Step 4 — Verify
# Container + streams sane:
ffprobe -v error -show_streams -show_format -of json "$OUT"
# For MP4-from-TS: confirm moov atom is present and AAC audio decodes:
ffprobe -v error -select_streams a:0 -show_entries stream=codec_name,sample_rate,channels "$OUT"
# For TS-from-MP4: confirm packets are Annex-B — first video packet should start 0x00 0x00 0x00 0x01:
ffmpeg -i "$OUT" -map 0:v:0 -c copy -f h264 - 2>/dev/null | xxd | head -1
# ^^^ raw Annex-B H.264
Playback test: open in VLC or ffplay. A bad bsf → corrupt / garbled video but valid
container structure (ffprobe will say "OK", playback will say "no").
Available scripts
scripts/bsf.py— subcommand-driven runner for the common bsf tasks (mp4-to-ts, ts-to-mp4, fix-packed-bframes, level, strip-sei, trace, zero-ts). Auto-detects H.264 vs HEVC viaffprobeand picks the matching*_mp4toannexbfilter.
Workflow
# Convert MP4 to TS (auto-detects codec):
uv run ${CLAUDE_SKILL_DIR}/scripts/bsf.py mp4-to-ts --input in.mp4 --output out.ts
# Convert TS to MP4 with aac_adtstoasc:
uv run ${CLAUDE_SKILL_DIR}/scripts/bsf.py ts-to-mp4 --input in.ts --output out.mp4
# Fix packed B-frames:
uv run ${CLAUDE_SKILL_DIR}/scripts/bsf.py fix-packed-bframes --input old.avi --output fixed.mp4
# Rewrite H.264 level to 4.1:
uv run ${CLAUDE_SKILL_DIR}/scripts/bsf.py level --input in.mp4 --output out.mp4 --level 4.1
# Strip SEI:
uv run ${CLAUDE_SKILL_DIR}/scripts/bsf.py strip-sei --input in.mp4 --output out.mp4
# Zero-base PTS/DTS on both streams:
uv run ${CLAUDE_SKILL_DIR}/scripts/bsf.py zero-ts --input in.mp4 --output out.mp4
# Dump headers:
uv run ${CLAUDE_SKILL_DIR}/scripts/bsf.py trace --input in.mp4 --stream v:0
All subcommands accept --dry-run (print the command, do not run) and --verbose
(pass -loglevel info to ffmpeg).
Reference docs
- Read
references/filters.mdfor: the full bsf reference table, codec ↔ bsf matrix, container-conversion cheat-sheet, H.264 / HEVC NAL-unit type numbers,settsexpression grammar, and the full option catalog forh264_metadata/hevc_metadata/av1_metadata/filter_units.
Gotchas
- bsf only works with
-c copy. Any re-encode path rewrites extradata itself and silently ignores the bsf. If your level-rewrite doesn't "stick", check you didn't accidentally pass-c:v libx264. - Applying
h264_mp4toannexbto an already-Annex-B stream used to corrupt output; ffmpeg 4.x+ auto-detects framing and becomes a no-op, but explicitly doing so on older builds is a footgun. When in doubt, probe source container first. - MP4 → TS/HLS may be auto-handled. Modern ffmpeg inserts
h264_mp4toannexb/hevc_mp4toannexbautomatically when the mpegts muxer sees AVCC input. Explicit is still safer for older builds and scripting reproducibility. - TS AAC → MP4 is NOT auto-handled.
aac_adtstoascmust be passed explicitly in most builds — otherwise you getMalformed AAC bitstream detectedor silent unplayable audio in the MP4. dump_extra=freq=kslightly inflates bitrate (SPS+PPS are tens of bytes per IDR), but it's what makes HLS / live segments joinable mid-stream.filter_units=remove_types=takes NAL unit type NUMBERS, not names. Seereferences/filters.mdfor the table. Multiple types are|-separated.h264_metadataonly works on H.264 streams — not HEVC, not AV1. Use the matchinghevc_metadata/av1_metadatafilter.- Chain multiple bsf with comma inside the same
-bsf:vflag:-bsf:v "h264_mp4toannexb,dump_extra=freq=k". Order matters — framing conversion first, then extradata manipulation, then metadata rewrite. - bsf argument syntax uses
=to separate the filter name from options, and:between options — always quote:-bsf:v "h264_metadata=level=4.1:aud=insert". - Stream specifiers matter:
-bsf:vtargets video streams,-bsf:aaudio,-bsf:ssubtitles. Use-bsf:v:0to target only the first video stream in a multi-stream file. settsexpressions run in packet timebase, NOT seconds. To add 2 seconds usets=PTS+2/TB, notts=PTS+2. Useffprobe -show_streams | grep time_baseif unsure.trace_headersrequires CBS support compiled into your ffmpeg build. Homebrew, Debian, and static builds from johnvansickle all ship it; some minimal stripped builds don't.noiseis destructive — intended for robustness testing of decoders, not for production use.mpeg4_unpack_bframesis safe on non-packed MP4s. It detects the absence of packed B-frames and passes through untouched, so it's cheap to apply prophylactically when batch-normalising old video libraries.
Examples
Example 1: Recorded .ts won't mux to .mp4 ("Malformed AAC bitstream")
ffmpeg -i recording.ts -c copy -bsf:a aac_adtstoasc recording.mp4
Example 2: Convert MP4 to HLS segments joinable mid-stream
ffmpeg -i movie.mp4 -c copy \
-bsf:v "h264_mp4toannexb,dump_extra=freq=k" \
-f hls -hls_time 6 -hls_segment_type mpegts out.m3u8
Example 3: Old DivX AVI has packed B-frames, fix without re-encoding
ffmpeg -i old_divx.avi -c copy -bsf:v mpeg4_unpack_bframes -f mp4 fixed.mp4
Example 4: Claim lower H.264 Level for a picky hardware decoder
ffmpeg -i in.mp4 -c copy -bsf:v "h264_metadata=level=4.0" decoderfriendly.mp4
Example 5: Strip all SEI (timecode / closed-captions metadata) NAL units
ffmpeg -i in.mp4 -c copy -bsf:v "filter_units=remove_types=6" stripped.mp4
Example 6: Zero-base PTS/DTS on a trimmed clip
ffmpeg -ss 00:01:30 -i src.mp4 -c copy -t 30 \
-bsf:v "setts=ts=PTS-STARTPTS" -bsf:a "setts=ts=PTS-STARTPTS" \
clip.mp4
Troubleshooting
Error: Malformed AAC bitstream detected: use the audio bitstream filter 'aac_adtstoasc'
Cause: muxing ADTS AAC (TS/FLV source) into MP4 without the bsf.
Solution: -bsf:a aac_adtstoasc.
Error: H.264 bitstream malformed, no startcode found, use the video bitstream filter 'h264_mp4toannexb'
Cause: copying AVCC-framed H.264 from MP4 into TS/FLV without converting framing.
Solution: -bsf:v h264_mp4toannexb.
Error: Bitstream filter 'h264_metadata' not found
Cause: your ffmpeg build was compiled without CBS support (rare — check ffmpeg -bsfs).
Solution: install a standard build (Homebrew / static johnvansickle / Debian ffmpeg).
Output has visual glitches after applying h264_mp4toannexb
Cause: source was already Annex-B (e.g. .ts), filter applied to already-converted stream.
Solution: probe source container first. On modern ffmpeg the filter no-ops safely, but
you may be on an older build — remove the flag when source is .ts / .flv.
-bsf:v appears to do nothing (e.g. level stays the same)
Cause: an -c:v ... re-encoder is active and overriding extradata.
Solution: use -c copy (or -c:v copy). bsf only runs on copied streams.
filter_units=remove_types= drops too much / too little
Cause: passing a type name instead of the numeric type, or using H.264 numbers against
HEVC (type spaces are different).
Solution: check references/filters.md for the type tables.
setts produces out-of-order DTS
Cause: expression doesn't preserve the DTS ≤ PTS invariant.
Solution: rewrite both — setts=pts=PTS-STARTPTS:dts=DTS-STARTDTS — and avoid
expressions that can push DTS past PTS on B-frame streams.