av
PyAV is a Python binding to the FFmpeg libraries, exposing containers, streams, packets, codecs, audio frames, and video frames as Python objects. It is built for applications that need precise, in-process access to encoded media and easy interchange with NumPy or Pillow. It is not a friendly video editor abstraction, and the project directly says that the `ffmpeg` command is often a better choice when it can do the job.
PyAV is the right tool when packet and frame access must live inside Python. For ordinary conversions and edits, follow the project's own advice and use the FFmpeg CLI or a higher-level library.
Use it if
- You need to inspect, seek, decode, encode, or remux media inside a Python process
- You need individual frames as Pillow images or NumPy arrays for analysis and transformation
- You need packet-level control that higher-level video editors hide
- You want FFmpeg capability without managing a subprocess protocol for every operation
- A single `ffmpeg` command already solves the task: the PyAV README says its lower-level API is likely to be a hindrance in that case
- You need a high-level editing timeline with clips, transitions, titles, and effects; PyAV exposes FFmpeg mechanics rather than an editor model
- You deploy to an unsupported platform or must compile from source without FFmpeg 8 development files and `pkg-config`; the README calls source installation complex
- Your host relies on Python sub-interpreters, including some WSGI configurations: the caveats page documents possible lockups from C callbacks
- Your team does not want to own codec, timestamp, pixel-format, packet-flushing, and container edge cases; the docs say underlying FFmpeg behavior often remains the user's responsibility
Setup reality
`pip install av` is easy only when pip selects an official wheel. Version 18 requires Python 3.11 or later, and the Linux, macOS, and Windows wheels bundle FFmpeg. If no compatible wheel exists, the project supports FFmpeg 8.x and expects its development headers plus `pkg-config`; on Windows the documented source workflow uses Conda and fetched vendor libraries. The API then exposes FFmpeg's own complexity: you must distinguish containers, streams, packets, and frames; preserve or rescale timestamps when remuxing; flush parsers and encoders with an empty input; pick codecs and pixel formats that the output container accepts; and close containers explicitly. The caveats page notes reference cycles that can delay automatic closing after thousands of opens, so context managers are not optional hygiene in loops. Default decoding uses slice threading. `AUTO` can increase throughput but also increases the delay between feeding packets and receiving frames. Python file objects and logging involve callbacks, and sub-interpreter use can lock up. Binary wheels reduce installation pain, but they do not turn media processing into a platform-neutral or low-judgment job.
Patterns
Inspect streams in a media containerinspect-container
import av
with av.open('input.mp4') as container:
print(container.format.name, container.duration)
for stream in container.streams:
print(stream.index, stream.type, stream.codec_context.name)Duration and stream metadata can be absent or inaccurate for live, damaged, or partially downloaded inputs.
Decode video frames as Pillow imagesdecode-video-frames
import av
with av.open('input.mp4') as container:
for frame in container.decode(video=0):
image = frame.to_image()
image.save(f'frame-{frame.index:06d}.jpg')Decoding every frame can produce far more data than the compressed source; stream or sample results instead of retaining all images.
Convert a video frame to RGB NumPy dataconvert-frame-to-numpy
import av
with av.open('input.mp4') as container:
frame = next(container.decode(video=0))
rgb = frame.to_ndarray(format='rgb24')
print(rgb.shape)Request the format your downstream model expects; implicit pixel-format conversions can add significant CPU cost.
Create a frame from a NumPy arraycreate-frame-from-numpy
import av
import numpy as np
rgb = np.zeros((720, 1280, 3), dtype=np.uint8)
frame = av.VideoFrame.from_ndarray(rgb, format='rgb24')
yuv = frame.reformat(format='yuv420p')Array shape, dtype, and declared format must agree; most H.264 encoders expect a YUV pixel format rather than RGB.
Decode only video keyframessave-keyframes
import av
with av.open('input.mp4') as container:
stream = container.streams.video[0]
stream.codec_context.skip_frame = 'NONKEY'
for index, frame in enumerate(container.decode(stream)):
frame.to_image().save(f'key-{index:04d}.jpg')This is fast for thumbnails and inspection, but keyframes are chosen by the encoder and are not evenly spaced.
Seek near a timestamp and decode forwardseek-video
import av
with av.open('input.mp4') as container:
stream = container.streams.video[0]
target_seconds = 30
container.seek(int(target_seconds / stream.time_base), stream=stream)
frame = next(container.decode(stream))
print(float(frame.pts * frame.time_base))Seeking normally lands on an earlier keyframe; decode forward and compare timestamps when an exact presentation time matters.
Remux a stream without transcodingremux-video
import av
with av.open('input.mp4') as source, av.open('output.mkv', 'w') as target:
in_stream = source.streams.video[0]
out_stream = target.add_stream_from_template(in_stream)
for packet in source.demux(in_stream):
if packet.size == 0:
continue
packet.stream = out_stream
target.mux(packet)Skip only zero-size flushing packets. The current cookbook warns that a valid reordered keyframe can have `dts is None`.
Encode generated frames to H.264encode-video
import av
import numpy as np
with av.open('output.mp4', 'w') as container:
stream = container.add_stream('libx264', rate=30)
stream.width, stream.height, stream.pix_fmt = 640, 360, 'yuv420p'
for _ in range(90):
frame = av.VideoFrame.from_ndarray(np.zeros((360, 640, 3), dtype=np.uint8), format='rgb24')
container.mux(stream.encode(frame))
container.mux(stream.encode(None))The final `encode(None)` flushes delayed frames; omitting it can truncate the end of the output.
Parse and decode a raw H.264 byte streamparse-raw-h264
import av
codec = av.CodecContext.create('h264', 'r')
with open('input.h264', 'rb') as source:
while True:
chunk = source.read(65536)
for packet in codec.parse(chunk):
for frame in codec.decode(packet):
print(frame.pts, frame.width, frame.height)
if not chunk:
break
for frame in codec.decode(None):
print(frame.pts)Feed an empty chunk through the parser and flush the decoder so buffered packets and delayed frames are emitted.
Enable automatic decoder threadingenable-frame-threading
import av
with av.open('input.mp4') as container:
stream = container.streams.video[0]
stream.thread_type = 'AUTO'
for frame in container.decode(stream):
process(frame)The cookbook says AUTO can be much faster, but it increases the delay between packet input and decoded-frame output.
Decode audio frames to NumPy arraysdecode-audio
import av
with av.open('input.m4a') as container:
for frame in container.decode(audio=0):
samples = frame.to_ndarray()
print(frame.sample_rate, frame.layout.name, samples.shape)The returned array layout depends on packed versus planar sample format; inspect `frame.format` before assuming channel axes.
Catch FFmpeg-backed failureshandle-media-errors
import av
try:
with av.open('possibly-broken.mp4') as container:
first = next(container.decode(video=0))
except (av.FFmpegError, StopIteration) as exc:
raise ValueError('Media could not produce a video frame') from excOpening can succeed even when decoding later fails; validate by consuming the streams your application actually needs.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| moviepy | PyPI | You want high-level clip editing, composition, text, and transitions |
| imageio-ffmpeg | PyPI | You mainly need FFmpeg-backed frame reading and writing with a smaller API |
| ffmpeg-python | PyPI | You prefer building an FFmpeg command graph and running the CLI as a subprocess |