How to Extract Video Metadata (Duration, Resolution, Codec, Audio) Using ffprobe
The claude-video skill extracts video metadata by calling ffprobe with JSON output flags, then parsing the format and stream data into a clean Python dictionary.
This article breaks down the implementation from the bradautomates/claude-video repository, where a single get_metadata function handles all ffprobe interactions for the watch skill. The approach favors portability across AI agent hosts by isolating binary dependencies and returning plain Python structures.
How ffprobe Video Metadata Extraction Works
The metadata pipeline follows four distinct phases: dependency verification, subprocess invocation, error handling, and structured parsing. This design keeps the skill functional across Claude Code, Codex, Cursor, and other agent environments without assuming pre-installed tools.
Step 1: Verify ffprobe Binary Availability
Before any subprocess call, the code confirms ffprobe exists on the system using shutil.which. This prevents opaque failures later in the pipeline.
import shutil
if not shutil.which("ffprobe"):
raise SystemExit("ffprobe not found. Please install FFmpeg / ffprobe.")
Source: [frames.py](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/frames.py#L86-L88)
This check appears at lines 86-88 and provides an actionable error message rather than a generic FileNotFoundError.
Step 2: Build and Execute the ffprobe Command
The get_metadata function constructs a subprocess call with three critical flags:
-v quiet— suppresses diagnostic output-print_format json— returns machine-parseable JSON-show_format -show_streams— includes container metadata and per-stream data
import subprocess
from pathlib import Path
result = subprocess.run(
[
"ffprobe",
"-v", "quiet",
"-print_format", "json",
"-show_format",
"-show_streams",
str(Path(video_path).resolve()),
],
capture_output=True,
text=True,
)
Source: [frames.py](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/frames.py#L90-L99)
Using Path.resolve() ensures absolute paths, avoiding working-directory ambiguity in subprocess contexts.
Step 3: Handle ffprobe Errors Gracefully
Non-zero exit codes trigger an immediate SystemExit with stderr content exposed to the caller:
if result.returncode != 0:
raise SystemExit(f"ffprobe failed: {result.stderr}")
Source: [frames.py](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/frames.py#L102)
This pattern lets calling code distinguish between missing binaries (caught earlier) and corrupt/unsupported video files (caught here).
Step 4: Parse ffprobe JSON Output for Metadata Fields
The parsing logic extracts six key fields by traversing the nested JSON structure:
| Field | JSON Path | Fallback Logic |
|---|---|---|
duration_seconds |
format.duration → video_stream.duration |
Defaults to 0.0 |
width |
video_stream.width |
None if absent |
height |
video_stream.height |
None if absent |
codec |
video_stream.codec_name |
None if absent |
size_bytes |
format.size |
Defaults to 0 |
has_audio |
Presence of audio stream | bool |
import json
data = json.loads(result.stdout)
streams = data.get("streams", [])
fmt = data.get("format", {})
video_stream = next(
(s for s in streams if s.get("codec_type") == "video"),
{}
)
audio_stream = next(
(s for s in streams if s.get("codec_type") == "audio"),
None
)
duration = float(
fmt.get("duration") or video_stream.get("duration") or 0
)
return {
"duration_seconds": duration,
"width": video_stream.get("width"),
"height": video_stream.get("height"),
"codec": video_stream.get("codec_name"),
"size_bytes": int(fmt.get("size") or 0),
"has_audio": audio_stream is not None,
}
Source: [frames.py](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/frames.py#L106-L119)
The duration fallback chain优先s container-level metadata (more reliable for variable-frame-rate files) over stream-level duration. The generator expression with next efficiently locates the first matching stream without full iteration.
Complete Usage Examples
Direct Library Import
from skills.watch.scripts.frames import get_metadata
meta = get_metadata("lecture.mp4")
print(f"Duration: {meta['duration_seconds']:.2f}s")
print(f"Resolution: {meta['width']}×{meta['height']}")
print(f"Video codec: {meta['codec']}")
print(f"Audio: {'present' if meta['has_audio'] else 'absent'}")
print(f"File size: {meta['size_bytes']:,} bytes")
Handling Missing Dependencies
import shutil
# Pre-check before calling get_metadata
if not shutil.which("ffprobe"):
print("Install FFmpeg: https://ffmpeg.org/download.html")
else:
meta = get_metadata("video.mov")
Integration in the Watch Skill
The top-level watch.py consumes get_metadata to calculate frame extraction budgets. Running:
claude watch https://youtube.com/watch?v=xyz
internally invokes the same function and renders:
[watch] video metadata: 2m13s, 1280×720, h264, audio present
Key Files in the Metadata Pipeline
| File | Purpose |
|---|---|
skills/watch/scripts/frames.py |
Core get_metadata implementation and frame extraction logic |
skills/watch/scripts/watch.py |
Orchestrates workflow; uses metadata for FPS calculations |
skills/watch/scripts/setup.py |
Validates binary dependencies before processing |
tests/test_fixtures.py |
Mirrors ffprobe patterns for test fixtures |
Summary
- Single function design:
get_metadatainframes.pyencapsulates all ffprobe interaction - JSON output parsing:
-print_format jsoneliminates fragile text parsing - Defensive parsing: Duration falls back from format to stream level; missing fields return
Noneor0 - Binary abstraction:
shutil.whichcheck keeps the skill portable across agent hosts - Clean return type: Plain
dictwith six standardized keys integrates seamlessly with downstream Python code
Frequently Asked Questions
What happens if ffprobe is not installed?
The code raises SystemExit with the message "ffprobe not found. Please install FFmpeg / ffprobe." at lines 86-88 of frames.py. This occurs before any subprocess attempt, providing immediate actionable feedback.
Why does duration use a fallback chain?
Container-level format.duration is generally more accurate for files with variable frame rates or editing metadata. The code tries this first, then falls back to video_stream.duration, and finally defaults to 0.0 if both are absent.
Can this detect multiple audio tracks?
The current implementation sets has_audio to True if any audio stream exists. It uses next() with a generator, which stops at the first match. To count tracks, modify the logic to sum(1 for s in streams if s.get("codec_type") == "audio").
Is the metadata extraction synchronous or asynchronous?
The claude-video skill uses subprocess.run for synchronous execution. For I/O-bound batch processing, you could adapt the pattern to asyncio.create_subprocess_exec while preserving the same ffprobe flag structure.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →