A typed Python wrapper for FFmpeg and ffprobe CLI tools. Build FFmpeg commands with a fluent API and parse ffprobe output into typed data structures.
- Python 3.10+
- FFmpeg and ffprobe installed and available on PATH
With uv:
uv add ffmpeg-wrap
With pip:
pip install ffmpeg-wrap
import ffmpeg_wrap as ffmpegresult = 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)ffmpeg.input("input.mkv").output("output.mp4", c="copy").overwrite_output().run()(
ffmpeg.input("input.mkv", ss=10, t=30)
.output("clip.mp4", vcodec="libx264", acodec="aac")
.overwrite_output()
.run()
)(
ffmpeg.input("input.mkv")
.output("output.mp4", c="copy")
.global_args("-hide_banner", "-loglevel", "error")
.overwrite_output()
.run()
)stdout, stderr = (
ffmpeg.input("input.mkv")
.output("output.mp4", c="copy")
.overwrite_output()
.run(capture_stdout=True, capture_stderr=True)
)validate() checks whether a file is valid media without parsing full probe output.
It returns a (ok, stderr) tuple instead of raising on bad media.
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) in addition to hard errors. Use a stricter
level when only fatal problems matter:
ok, stderr = ffmpeg.validate("video.mkv", loglevel="error")
ok, stderr = ffmpeg.validate("video.mkv", loglevel="fatal")Pass extra ffprobe flags via extra_args:
ok, stderr = ffmpeg.validate("video.mkv", extra_args=("-hide_banner",))Use validate() when you only need a pass/fail check. Use probe() when you need
stream and format metadata. The only exception validate() raises is FFmpegError
when the ffprobe executable could not be run (missing, not executable, etc.).
try:
ok, stderr = ffmpeg.validate("video.mkv")
except ffmpeg.FFmpegError:
print("ffprobe could not be executed")try:
ffmpeg.probe("nonexistent.mkv")
except ffmpeg.FFmpegError as e:
print(f"Probe failed: {e}")
try:
ffmpeg.input("missing.mkv").output("out.mp4").run()
except ffmpeg.FFmpegError as e:
print(f"FFmpeg failed: {e}")FFmpegError carries structured introspection so you can classify a failure
without re-parsing the message string. str(e) is still 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:
# Branch on the exit code / inspect stderr instead of scraping str(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 (e.g. retrying only
on transient exit codes): the wrapper stays unopinionated about which failures
are retryable and exposes the raw returncode/stderr for the caller to decide.
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()Every chain begins with ffmpeg.input(...), which returns the FFmpeg builder.
There is no separate output()/graph() entry point: outputs, filters, and
global flags are all methods on the chain rooted at an input.
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.wavUse filter_complex() for a graph-level -filter_complex and map() to wire
its labelled outputs to multiple output files. The filtergraph is emitted in a
dedicated slot (after global args, before inputs), so its placement no longer
depends on which output it is attached to:
(
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()
)
# ffmpeg -filter_complex [0:v]split=2[full][thumb];[thumb]scale=320:-2[thumb] \
# -i input.mkv -map [full] full.mp4 -map [thumb] thumb.mp4For large graphs, read them from a file with filter_complex_script() (mutually
exclusive with filter_complex() at runtime):
(
ffmpeg.input("input.mkv")
.filter_complex_script("graph.txt")
.output("output.mp4")
.run()
)
# ffmpeg -filter_complex_script graph.txt -i input.mkv output.mp4map() is repeatable and accepts raw specifiers, so a stream-preserving remux
emits one -map per stream type:
(
ffmpeg.input("input.mkv")
.output("output.mkv", c="copy")
.map("0:v").map("0:a").map("0:s")
.overwrite_output()
.run()
)
# ffmpeg -i input.mkv -c copy -map 0:v -map 0:a -map 0:s output.mkvThe same expansion works by passing a list directly: output("out.mkv", map=["0:v", "0:a", "0:s"]). map(stream) accepts a Stream from probe() and
emits its per-type specifier (e.g. 0:s:0); map_stream("s", 0) builds the
specifier explicitly.
When embedding a path in a filtergraph (e.g. subtitles=), escape it with
filter_arg_escape() so colons and backslashes in the path do not break the
graph:
path = r"C:\videos\clip.srt"
graph = f"subtitles={ffmpeg.filter_arg_escape(path)}"
# subtitles='C\:\\videos\\clip.srt'Discover what the installed ffmpeg build actually supports at runtime instead of
guessing. encoders() returns the full set of encoder names (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 decode/acceleration backend with the hwaccel input option,
or the discoverable hwaccel() sugar — both emit -hwaccel before the input's
-i:
(
ffmpeg.input("input.mkv")
.hwaccel("cuda")
.output("output.mp4")
.codec("v", video_codec)
.overwrite_output()
.run()
)
# ffmpeg -hwaccel cuda -i input.mkv -c:v h264_nvenc output.mp4
# Equivalent, set directly on the input:
ffmpeg.input("input.mkv", hwaccel="cuda").output("output.mp4").codec("v", video_codec)The wrapper stays unopinionated about encoder tuning: it reports what is available and lets you choose the codec and parameters.
Run ffprobe on a file and return a typed ProbeResult.
filename-- Path to the media file (str or PathLike).ffprobe_path-- Path to the ffprobe executable. Defaults to"ffprobe".- Returns:
ProbeResultwithstreamsandformatfields. - Raises:
FFmpegErroron subprocess failure or invalid output.
Run ffprobe in validation mode and check for errors/warnings.
filename-- Path to the media file (str or PathLike).ffprobe_path-- Path to the ffprobe executable. Defaults to"ffprobe".loglevel-- Value passed to ffprobe's-vflag. Defaults to"warning"(surfaces DTS/codec warnings). Use"error"to ignore warnings or"fatal"/"panic"for only unrecoverable failures.extra_args-- Additional raw arguments forwarded to ffprobe before the filename, e.g.("-hide_banner",).- Returns:
tuple[bool, str]--(ok, stderr_text).okisTruewhen ffprobe exits with code 0 and stderr is empty after stripping whitespace. - Raises:
FFmpegErroronly when the ffprobe executable could not be run. Does not raise on invalid media.
Create a new FFmpeg builder chain starting with an input file.
filename-- Input file path.ffmpeg_path-- Path to the ffmpeg executable. Defaults to"ffmpeg".**kwargs-- Input options passed as FFmpeg flags (e.g.,ss=10,t=30).- Returns:
FFmpeginstance for method chaining.
Fluent builder for FFmpeg commands.
input(filename, **kwargs)-- Add an input file with options.output(filename, **kwargs)-- Add an output file with options. List/tuple values expand to a repeated flag (e.g.map=["0:v", "0:a"]).overwrite_output()-- Add the-yflag to overwrite existing output files.global_args(*args)-- Add global arguments before inputs.map(*specs)-- Append one-mapper spec on the current output. Accepts raw specifiers ("0:v") or aStream(emits its per-type specifier). Repeatable.map_stream(kind, ordinal, input=0)-- Append-map {input}:{kind}:{ordinal}(e.g.map_stream("s", 0)->-map 0:s:0).codec(kind, name)---c:{kind} {name}on the current output (e.g.codec("v", "libx264")).bitrate(kind, value)---b:{kind} {value}.quality(kind, value)---q:{kind} {value}.audio_filter(chain)/video_filter(chain)---filter:a/-filter:von the current output.flag(*names)-- Append bare-{name}switches (e.g.flag("vn", "sn")).filter_complex(graph_str)-- Graph-level-filter_complex, emitted in a dedicated slot after global args and before inputs.filter_complex_script(path)-- Graph-level-filter_complex_script(mutually exclusive withfilter_complex()at runtime).hwaccel(name)-- Input-side-hwaccel {name}sugar, emitted before-i.hide_banner()---hide_bannerglobal flag.loglevel(level)---loglevel {level}global flag.compile()-- Build the command as a list of strings without executing it.run(capture_stdout=False, capture_stderr=False, text=False)-- Execute the command. Returns(stdout, stderr). Withtext=Truethe captured values arestr; otherwisebytes(orNonewhen the corresponding capture flag isFalse). RaisesFFmpegErroron failure.
Typed result from ffprobe.
streams-- List ofStreamobjects.format-- OptionalFormatobject.duration_seconds()--float | None. Delegates toformat.duration_seconds();Nonewhen there is no format.
A single stream from ffprobe output. Fields include index, codec_name, codec_type, type_index, width, height, channels, sample_rate, duration, bit_rate, tags, disposition.
map_specifier(input_index=0)-- The per-type-mapspecifier for this stream (e.g.0:s:0), falling back to the absolute-index form for unknowncodec_type.duration_seconds()--float | None. Parsesduration, returningNonefor missing/"N/A"/non-numeric values.is_video/is_audio/is_subtitle/is_data/is_attachment-- Properties comparingcodec_typeagainstCodecType. AllFalsefor unknown/None.is_text_subtitle/is_image_subtitle-- Best-effort properties classifying a subtitle stream bycodec_name.
The format section from ffprobe output. Fields include filename, nb_streams, nb_programs, format_name, format_long_name, start_time, duration, size, bit_rate, probe_score, tags.
duration_seconds()--float | None. Parsesduration, returningNonefor missing/"N/A"/non-numeric values (the rawduration: stris preserved).
StrEnum of the known ffprobe codec_type values (VIDEO, AUDIO, SUBTITLE, DATA, ATTACHMENT). Used by the Stream predicates. Stream.codec_type stays typed str | None so exotic values still decode without ValidationError — compare against these members rather than retyping the field.
Return the set of encoder names supported by the installed ffmpeg build.
- Parses
ffmpeg -hide_banner -encoders; the result is cached perffmpeg_path. - Returns:
frozenset[str].
bool. Whether name is in encoders(ffmpeg_path). Use this instead of hard-coding encoder availability.
str. Escape a value for use inside a filtergraph argument (e.g. a subtitles= path), handling \, :, and ', and wrapping the result in single quotes.
Exception raised when FFmpeg or ffprobe operations fail. str(e) is the human-readable message; the keyword-only attributes stderr (str | None), returncode (int | None), and cmd (list[str] | None) describe the underlying process failure and are None when not applicable.