Guide¶
This page walks through the synchronous API. For exhaustive signatures and field documentation, see the auto-generated API Reference.
import ffmpeg_wrap as ffmpeg
Probe a media file¶
result = ffmpeg.probe("video.mkv")
for stream in result.streams:
print(stream.codec_name, stream.codec_type)
if result.format:
print(result.format.duration)
print(result.format.format_name)
probe() raises FFmpegError on subprocess failure or invalid output.
Bit depth¶
Stream carries the three raw fields ffprobe reports for sample width
(sample_fmt, bits_per_sample, bits_per_raw_sample), and bit_depth()
resolves them the way ffprobe populates them: bits_per_raw_sample when it is
present and numeric, else a positive bits_per_sample, else None:
for stream in result.streams:
if stream.is_audio:
print(stream.sample_fmt, stream.bits_per_sample, stream.bits_per_raw_sample)
print(stream.bit_depth())
The value is the width ffprobe reports and says nothing about lossiness.
16-bit WAV and FLAC answer 16; 24-bit PCM and FLAC answer 24, although both
report sample_fmt="s32", which is why sample_fmt alone is not enough; IMA
ADPCM answers 4 and ยต-law answers 8 because those codecs have a real coded
width; MP3 and Opus answer None because they carry no fixed width.
Video streams answer their pixel bit depth, because ffprobe reports it in
bits_per_raw_sample too: 8 for 8-bit H.264, 10 for yuv420p10le.
sample_fmt and bits_per_sample stay None for video.
Build and run a command¶
Every chain starts at ffmpeg.input(...), which returns the FFmpeg builder.
Outputs, filters, and global flags are all methods on that chain.
ffmpeg.input("input.mkv").output("output.mp4", c="copy").overwrite_output().run()
Input/output options are passed as keyword arguments:
ffmpeg.input("input.mkv", ss=10, t=30).output("clip.mp4", vcodec="libx264", acodec="aac").overwrite_output().run()
Use global_args() for flags before the inputs, and run(capture_stdout=...,
capture_stderr=...) to capture process output.
Synthetic sources (test patterns, silence, color) are inputs too. Pass the
lavfi virtual device as the input and the source description as its
"filename":
(
ffmpeg.input("anullsrc=channel_layout=stereo:sample_rate=48000", f="lavfi", t=5)
.output("silence.wav")
.overwrite_output()
.run()
)
# ffmpeg -f lavfi -t 5 -i anullsrc=channel_layout=stereo:sample_rate=48000 silence.wav
By default the builder runs the ffmpeg/ffprobe executables found on PATH.
If they live elsewhere, point the builder and the probe helpers at the
executables explicitly with ffmpeg_path and ffprobe_path:
result = ffmpeg.probe("video.mkv", ffprobe_path="/usr/local/bin/ffprobe")
ok, stderr = ffmpeg.validate("video.mkv", ffprobe_path="/usr/local/bin/ffprobe")
ffmpeg.input("input.mkv", ffmpeg_path="/usr/local/bin/ffmpeg").output("output.mp4").run()
Mapping and complex graphs¶
map() is repeatable and accepts raw specifiers or a Stream from probe().
Use filter_complex() for a graph-level -filter_complex and wire labelled
outputs to multiple files:
(
ffmpeg.input("input.mkv")
.filter_complex("[0:v]split=2[full][thumb];[thumb]scale=320:-2[thumb]")
.output("full.mp4")
.map("[full]")
.output("thumb.mp4")
.map("[thumb]")
.overwrite_output()
.run()
)
For large graphs, read them from a UTF-8 file with filter_complex_script().
The file is read when the method is called and its contents are passed through
the portable -filter_complex option. It is mutually exclusive with
filter_complex() at runtime, so use one or the other:
ffmpeg.input("input.mkv").filter_complex_script("graph.txt").output("output.mp4").run()
# ffmpeg -filter_complex "<contents of graph.txt>" -i input.mkv output.mp4
When embedding a path inside a filtergraph (e.g. subtitles=), escape it with
filter_arg_escape():
path = r"C:\videos\clip.srt"
graph = f"subtitles={ffmpeg.filter_arg_escape(path)}"
# subtitles='C\:\\videos\\clip.srt'
Validate a media file¶
validate() checks whether a file is valid media and returns a (ok, stderr)
tuple instead of raising on bad media. It raises FFmpegError only when the
ffprobe executable itself could not be run, or FFmpegTimeoutError when a
timeout= expires (see Timeouts).
ok, stderr = ffmpeg.validate("video.mkv")
if not ok:
print(f"Invalid media: {stderr}")
The default loglevel="warning" surfaces ffprobe warnings (non-monotonic DTS,
unsupported codecs, truncated frames). Use "error" or "fatal" when only
hard failures matter, and pass extra ffprobe flags via extra_args.
Encoder discovery¶
Discover what the installed ffmpeg build supports at runtime instead of
hard-coding it. encoders() returns the full set (cached per ffmpeg path);
has_encoder() is a single-name membership check:
if ffmpeg.has_encoder("h264_nvenc"):
video_codec = "h264_nvenc"
else:
video_codec = "libx264"
Request a hardware acceleration backend with hwaccel() (emits -hwaccel
before the input's -i):
ffmpeg.input("input.mkv").hwaccel("cuda").output("output.mp4").codec("v", video_codec).overwrite_output().run()
Error handling¶
FFmpegError carries structured introspection so you can classify a failure
without re-parsing the message. str(e) is the human-readable message; the
returncode, stderr, and cmd attributes describe the underlying process
failure (each is None when not applicable):
try:
ffmpeg.input("input.mkv").output("output.mp4", c="copy").overwrite_output().run()
except ffmpeg.FFmpegError as e:
if e.returncode == 1 and e.stderr and "No space left" in e.stderr:
raise
print(f"ffmpeg exited {e.returncode}: {e.stderr}")
This is the building block for consumer-side retry policies: the wrapper stays unopinionated about which failures are retryable.
Timeouts¶
run(), probe() and validate() accept a keyword-only timeout in seconds;
None, the default, means no limit. When it expires the child is killed,
reaped, and FFmpegTimeoutError is raised:
try:
ffmpeg.input("input.mkv").output("output.mp4", c="copy").overwrite_output().run(timeout=60)
except ffmpeg.FFmpegTimeoutError as e:
print(f"gave up after {e.timeout}s: {e.stderr}")
result = ffmpeg.probe("video.mkv", timeout=10)
ok, stderr = ffmpeg.validate("video.mkv", timeout=10)
FFmpegTimeoutError subclasses FFmpegError, so except FFmpegError keeps
catching it. It carries timeout (the limit that expired), cmd, and
stderr: the tail collected before the kill, decoded the same way the failure
path decodes it, so it is str | None in both text modes. returncode is
always None, because the run did not complete within the limit and a kill
status is not an ffmpeg exit code. A timeout that is not a positive finite number, or one above 2147483.647 seconds (the largest
value the subprocess timers accept on every platform), raises ValueError before any process starts.
The kill applies to the process launched from ffmpeg_path (or
ffprobe_path); descendants of a wrapper script are not killed. Without
capture_stderr=True the limit also bounds the whole call rather than just
ffmpeg's lifetime: the wrapper keeps draining ffmpeg's stdout and stderr until
they close, so a background process that a wrapper script leaves holding either
pipe turns into FFmpegTimeoutError once the deadline, plus a one-second grace
for the last buffered output, has passed, even though ffmpeg itself exited and
nothing was killed. After a kill the wrapper waits up to five seconds for the
forwarding threads before raising.
The timeout bounds ffmpeg, not your stderr sink
Without capture_stderr=True, ffmpeg's stderr is forwarded live to the
inherited sys.stderr with a synchronous write. If that sink itself blocks
(a full pipe nobody reads, a stopped terminal), the timeout cannot
interrupt the write in progress: after the kill the wrapper waits a bounded
time for the forwarding thread and raises regardless, but the blocked write
is not unblocked. Callers with a slow or blocking stderr sink should pass
capture_stderr=True, which sends stderr to a buffer instead.