Skip to main content

Typed fluent FFmpeg/ffprobe wrapper with msgspec models

Project description

ffmpeg-wrap

Tests Prek checks PyPI Downloads

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.

Requirements

  • Python 3.10+
  • FFmpeg and ffprobe installed and available on PATH

Installation

With uv:

uv add ffmpeg-wrap

With pip:

pip install ffmpeg-wrap

Usage

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)

Build and run an FFmpeg command

ffmpeg.input("input.mkv").output("output.mp4", c="copy").overwrite_output().run()

Multiple inputs and outputs

(
    ffmpeg.input("input.mkv", ss=10, t=30)
    .output("clip.mp4", vcodec="libx264", acodec="aac")
    .overwrite_output()
    .run()
)

Global arguments

(
    ffmpeg.input("input.mkv")
    .output("output.mp4", c="copy")
    .global_args("-hide_banner", "-loglevel", "error")
    .overwrite_output()
    .run()
)

Capture output

stdout, stderr = (
    ffmpeg.input("input.mkv")
    .output("output.mp4", c="copy")
    .overwrite_output()
    .run(capture_stdout=True, capture_stderr=True)
)

Validate a media file

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")

Error handling

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.

Custom executable paths

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()

Pipelines always start at input()

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.wav

Complex pipelines

Use 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.mp4

For 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.mp4

map() 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.mkv

The 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'

Hardware acceleration

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.

API Reference

probe(filename, ffprobe_path="ffprobe")

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: ProbeResult with streams and format fields.
  • Raises: FFmpegError on subprocess failure or invalid output.

validate(filename, ffprobe_path="ffprobe", loglevel="warning", extra_args=())

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 -v flag. 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). ok is True when ffprobe exits with code 0 and stderr is empty after stripping whitespace.
  • Raises: FFmpegError only when the ffprobe executable could not be run. Does not raise on invalid media.

input(filename, ffmpeg_path="ffmpeg", **kwargs)

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: FFmpeg instance for method chaining.

FFmpeg

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 -y flag to overwrite existing output files.
  • global_args(*args) -- Add global arguments before inputs.
  • map(*specs) -- Append one -map per spec on the current output. Accepts raw specifiers ("0:v") or a Stream (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:v on 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 with filter_complex() at runtime).
  • hwaccel(name) -- Input-side -hwaccel {name} sugar, emitted before -i.
  • hide_banner() -- -hide_banner global 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). With text=True the captured values are str; otherwise bytes (or None when the corresponding capture flag is False). Raises FFmpegError on failure.

ProbeResult

Typed result from ffprobe.

  • streams -- List of Stream objects.
  • format -- Optional Format object.
  • duration_seconds() -- float | None. Delegates to format.duration_seconds(); None when there is no format.

Stream

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 -map specifier for this stream (e.g. 0:s:0), falling back to the absolute-index form for unknown codec_type.
  • duration_seconds() -- float | None. Parses duration, returning None for missing/"N/A"/non-numeric values.
  • is_video / is_audio / is_subtitle / is_data / is_attachment -- Properties comparing codec_type against CodecType. All False for unknown/None.
  • is_text_subtitle / is_image_subtitle -- Best-effort properties classifying a subtitle stream by codec_name.

Format

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. Parses duration, returning None for missing/"N/A"/non-numeric values (the raw duration: str is preserved).

CodecType

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.

encoders(ffmpeg_path="ffmpeg")

Return the set of encoder names supported by the installed ffmpeg build.

  • Parses ffmpeg -hide_banner -encoders; the result is cached per ffmpeg_path.
  • Returns: frozenset[str].

has_encoder(name, ffmpeg_path="ffmpeg")

bool. Whether name is in encoders(ffmpeg_path). Use this instead of hard-coding encoder availability.

filter_arg_escape(value)

str. Escape a value for use inside a filtergraph argument (e.g. a subtitles= path), handling \, :, and ', and wrapping the result in single quotes.

FFmpegError

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ffmpeg_wrap-0.3.0.tar.gz (49.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ffmpeg_wrap-0.3.0-py3-none-any.whl (20.3 kB view details)

Uploaded Python 3

File details

Details for the file ffmpeg_wrap-0.3.0.tar.gz.

File metadata

  • Download URL: ffmpeg_wrap-0.3.0.tar.gz
  • Upload date:
  • Size: 49.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for ffmpeg_wrap-0.3.0.tar.gz
Algorithm Hash digest
SHA256 122fdd68a75cde5903d563128401ed5ae0a73d917f904ed7e274d3cd9d71e52f
MD5 c28b8bcf3869a1dd632811b2fe601820
BLAKE2b-256 3c7bd23398fe4435b3dc0e23819fe346ed486d4ba8ad8c0103f5087da74a1608

See more details on using hashes here.

File details

Details for the file ffmpeg_wrap-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: ffmpeg_wrap-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 20.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for ffmpeg_wrap-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 96e9d77ba16130d42620ccc937e9d53a0e8b44987fa7d5c17f253b587af01bea
MD5 73014806c159e0b17da8a4c7f31a43e6
BLAKE2b-256 feabfa9cf8d366a5ad0f69260cdf6c3c73e04f384568554fb44bb052f3e18b91

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page