Extract video metadata as JSON with FFprobe, MediaInfo, or yt-dlp
Copy-paste ffprobe, MediaInfo, and yt-dlp commands to extract duration, codecs, resolution, bitrate, chapters, and YouTube metadata as JSON — plus batch scripts for whole folders.
To extract metadata from a video file, run ffprobe -v quiet -print_format json -show_format -show_streams video.mp4. The JSON includes duration, bitrate, container tags, video codec, resolution, framerate, audio codec, sample rate, channel layout, and stream-level metadata.
Video metadata splits into two layers. The container (MP4, MOV, MKV, AVI) carries duration, codecs, bitrate, and any custom tags. The streams inside carry per-stream metadata: video resolution, framerate, color space, audio sample rate, language tags. Both matter, and there's a right tool for each.
FFprobe: the reference tool
Comes with FFmpeg. The one-liner that gives you everything as JSON:
ffprobe -v quiet -print_format json -show_format -show_streams video.mp4
Pipe to jq for filtering. -show_chapters adds chapter markers if present. The output covers duration, bitrate, codec name and parameters, color profile, audio channel layout — every field a media pipeline cares about.
MediaInfo: nicer output for humans
Cross-platform GUI and CLI. mediainfo --Output=JSON video.mp4 produces output similar to ffprobe but with friendlier field names. The GUI is good for one-off inspection; the CLI is good for batch.
yt-dlp: metadata from a URL without downloading
yt-dlp -j "https://youtube.com/watch?v=..." returns a single JSON blob with title, description, upload date, duration, view count, like count, channel info, and every available format/quality/codec combination. The --write-info-json flag saves it alongside a download.
Useful for pipelines that catalog videos before deciding whether to download them, or that just need YouTube/Vimeo metadata for a search index.
Python: ffmpeg-python or pymediainfo
import json, subprocess out = subprocess.check_output([ "ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "video.mp4", ]) meta = json.loads(out) print(meta["format"]["duration"], meta["streams"][0]["width"])
Wrapping ffprobe in subprocess is usually simpler than the dedicated bindings. If you're processing thousands of files, ffmpeg-python's Probe interface is a bit tidier.
What's worth extracting
- Duration, framerate, resolution — for catalog and search
- Codec and bitrate — for transcode planning
- Audio language tracks — for subtitle/dub workflows
- Creation date — sometimes carried in QuickTime/MOV containers, often not in MP4
- Camera make/model — present in iPhone/Android-recorded video, accessible via -show_entries stream_tags
What you can't get
Speech transcripts aren't metadata — they're content. For those, see the YouTube transcript post or run the audio through Whisper. Same for visual content: object detection, OCR on visible frames, scene changes — these all require separate processing pipelines on the actual video stream.
FFprobe field reference
| Field path | What it tells you | Typical use |
|---|---|---|
| format.duration | Length in seconds | Catalog, billing, trim planning |
| format.bit_rate | Overall bitrate | Quality check, CDN sizing |
| format.tags.creation_time | Container creation timestamp | Archival sorting (often missing in re-encoded MP4) |
| streams[n].codec_name | h264, hevc, aac, etc. | Transcode compatibility |
| streams[n].width / height | Pixel dimensions | Thumbnail generation, aspect ratio |
| streams[n].r_frame_rate | Framerate | Sync, slow-motion detection |
| streams[n].tags.rotate | Display rotation (mobile) | Fix sideways playback before publishing |
| streams[n].tags.language | Audio/subtitle track language | Localization workflows |
Batch: metadata for every file in a folder
for f in *.mp4; do ffprobe -v quiet -print_format json -show_format -show_streams "$f" > "${f%.mp4}.json" done
For a single combined catalog, jq-merge the outputs or use a Python loop that appends to a list and writes catalog.json at the end. Add -show_entries format_tags if you only need container tags and want smaller JSON.
exiftool vs ffprobe for camera metadata
Phone-recorded MOV/MP4 often carries EXIF-like tags (Make, Model, GPS) that ffprobe surfaces under stream_tags but exiftool presents more readably:
exiftool -json -G video.mp4 | jq '.[0] | {Make, Model, CreateDate, GPSLatitude, GPSLongitude}'
Use exiftool when the question is "what device shot this?" Use ffprobe when the question is "what codecs and bitrates do I have?" For YouTube URLs, skip both and use yt-dlp -j.
Rotation and display matrix
iPhone videos often store 1920×1080 dimensions with a rotate=90 tag. Players apply the rotation at display time; naive thumbnail extractors do not. Check streams[].side_data_list for Display Matrix or the rotate tag before assuming width/height match what viewers see.
Building a catalog.json from a folder
For a media DAM or transcode queue, you want one JSON array with the fields your pipeline actually uses — not the full ffprobe dump:
import json, subprocess, pathlib def probe(path): out = subprocess.check_output([ "ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", str(path), ]) meta = json.loads(out) v = next(s for s in meta["streams"] if s["codec_type"] == "video") a = next((s for s in meta["streams"] if s["codec_type"] == "audio"), None) return { "file": path.name, "duration_s": float(meta["format"]["duration"]), "width": v["width"], "height": v["height"], "vcodec": v["codec_name"], "acodec": a["codec_name"] if a else None, } catalog = [probe(p) for p in pathlib.Path(".").glob("*.mp4")] pathlib.Path("catalog.json").write_text(json.dumps(catalog, indent=2))
Skip files where ffprobe exits non-zero — truncated downloads and zero-byte placeholders are common in upload folders.
ffprobe troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| duration is N/A | Live stream or incomplete file | Wait for download to finish; use -show_packets for partial files |
| No audio stream listed | Silent video or separate audio file | Check for external .m4a; merge with ffmpeg -i |
| codec_name unknown | Proprietary or corrupt stream | Try ffmpeg -i for human-readable error |
| creation_time null on MP4 | Re-encoded without tag copy | Read exiftool CreateDate on source camera file |
| width/height swapped | Rotation tag present | Read tags.rotate or side_data display matrix |
Choosing fields for downstream systems
Transcode planners care about codec, profile, pixel format, and color range (tv vs pc). CDN sizing cares about bitrate and duration. Search indexes want title and description from container tags or yt-dlp for web sources. Don't store the entire ffprobe JSON in Postgres — extract the dozen fields you query on and keep the raw dump in object storage if you need forensics.
Frequently asked questions
Does ffprobe work on MKV, MOV, and WebM?+
Yes — ffprobe reads any container FFmpeg supports. WebM metadata is sparser than MOV; don't expect camera tags in browser-recorded WebM.
Can I get metadata without installing FFmpeg?+
MediaInfo has a standalone installer with no FFmpeg dependency. On macOS, brew install mediainfo. For YouTube-only workflows, yt-dlp -j is enough.
Why is creation_time missing from my MP4?+
Re-encoding strips or overwrites container tags. The original camera timestamp may only exist in the source MOV. Always extract metadata from the earliest file in your pipeline.