mrkeyoor.com_
Sat 08 Aug 17:39 UTC
PyPIUtilsupdated 08 Aug 2026

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.

Verdict

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.

API stability3/5Core concepts such as `av.open`, `Container.decode`, `demux`, frame conversion, and stream encoding are established, but the package intentionally tracks FFmpeg closely and major releases can raise Python and FFmpeg requirements. Version 18 now requires Python 3.11 and supports FFmpeg 8.x, so deployment and codec behavior deserve testing on every major upgrade even when Python call shapes look familiar.
Docs4/5The official 18.0.0 documentation has API references, cookbook examples for keyframes, remuxing, parsing, threading, and capture, plus a notably honest caveats page. It also admits that statements about underlying FFmpeg are not always authoritative. The material is valuable for experienced media developers, but beginners still need FFmpeg concepts that the PyAV docs cannot fully teach.
Maintenance5/5Version 18.0.0 shipped in July 2026, the repository was pushed on August 8, 2026, and GitHub reports only 6 open issues and pull requests alongside 3,258 stars. Current wheels target the three main desktop and server operating-system families and bundle FFmpeg, showing active release engineering rather than a thin wrapper left to downstream packagers.
Ecosystem4/5PyAV recorded 7,331,079 downloads in the latest measured week and connects decoded media directly to NumPy and Pillow, two standard Python data and imaging tools. Its ecosystem is effectively FFmpeg's huge codec and container surface. That reach is valuable, but compatibility still depends on the codecs compiled into the bundled or system FFmpeg build and on platform capture backends.

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
Skip it if

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 exc

Opening can succeed even when decoding later fails; validate by consuming the streams your application actually needs.

Alternatives

PackageRegistryPick it when
moviepyPyPIYou want high-level clip editing, composition, text, and transitions
imageio-ffmpegPyPIYou mainly need FFmpeg-backed frame reading and writing with a smaller API
ffmpeg-pythonPyPIYou prefer building an FFmpeg command graph and running the CLI as a subprocess