This release is a pre-release and may not be stable for production use.
pyopengl-video
Hardware video encoding of OpenGL colour buffers, with the frame never leaving the GPU.
The renderer has already put the frame in GPU memory, and the GPU has a video
encoder on the same die. pyopengl-video hands one to the other: a texture goes
in, an H.264 stream comes out, and nothing crosses the bus but the compressed
result.
from pyopengl_video import open_encoder
from pyopengl_video.mp4 import MP4Writer
with open_encoder(1920, 1080, fps=60, bitrate=12_000_000) as encoder:
handle = encoder.new_input()
with MP4Writer('out.mp4', encoder) as movie:
for index in range(600):
render()
with handle.for_drawing():
copy_the_frame_into(handle.framebuffer)
movie.write(encoder.encode(handle, timestamp=index * 1500))
movie.write(encoder.flush())
examples/record_triangle.py is that loop around a real renderer, start to
finish.
What it needs
Python 3.10 or newer, PyOpenGL, and a GPU whose encoder it knows about. Nothing else: the encoder is the one in the graphics driver, loaded by name, and the MP4 muxer is part of this package.
| Backend | Hardware | Platform | Codec | Frame handed over as | State |
|---|---|---|---|---|---|
nvenc |
NVIDIA, Kepler and later | Linux | H.264 | an OpenGL texture, in place | working |
vaapi |
AMD, VCN | Linux | H.264 | a DMA-BUF exported from a texture | working |
vpl |
Intel, Gen9 and later | Windows | H.264 | a Direct3D 11 surface OpenGL draws into | working |
vaapi |
Intel, Gen9 and later | Linux | H.264 | the same DMA-BUF | the same code, untested |
nvenc |
NVIDIA | Windows | H.264 | the same Direct3D 11 surface | next |
amf |
AMD | Windows | H.264 | the same Direct3D 11 surface | planned |
The frame reaches the encoder by whatever handle the platform has for one.
NVIDIA takes an OpenGL texture by name, but only on Linux; Intel and AMD on
Linux take a DMA-BUF exported from one; on Windows every vendor's encoder takes
a Direct3D 11 texture, and WGL_NV_DX_interop2 makes one allocation that is a
Direct3D texture and an OpenGL texture at the same time. The muxer, the
interface and the recorder are the same either way.
The vaapi backend needs an EGL context. Exporting a texture as a DMA-BUF
is an EGL extension, and GLFW makes a GLX context by default on X11, so ask for
one before the window is created:
glfw.window_hint(glfw.CONTEXT_CREATION_API, glfw.EGL_CONTEXT_API)
A context that cannot export is one this backend cannot record from, so it
reports itself unavailable rather than failing later; encoders() returning
nothing on a machine that should have an encoder is the first thing this
explains. It also needs a VA-API driver for the GPU installed beside libva
itself — mesa-va-drivers for AMD, intel-media-va-driver for Intel.
Zero-copy needs the encoder on the same GPU as the renderer. On a machine with more than one, the backends match the OpenGL context's adapter and offer themselves only there — see the plan.
Ask what a machine can do:
from pyopengl_video import encoders
for backend in encoders():
print(backend.name, backend.vendor, sorted(backend.codecs), backend.max_size)
Two things to know before recording your own renderer
The picture comes out upside down unless the copy turns it over. OpenGL's
framebuffer starts at the bottom left; the encoder reads a texture from its
first row and calls that the top of the picture. A glBlitFramebuffer with its
destination Y coordinates reversed flips the frame as it copies, at no cost —
capture() in the example does exactly that.
One texture is not enough. An encoder that reorders frames is still reading
a texture after encode() has returned. Register encoder.input_slots textures
and cycle through them; handing back one the encoder still holds raises an error
that says so.
Timing, colour and reordering
Timestamps and durations are in the encoder's timescale, 90 kHz by default,
and the encoder echoes back what it is given — the caller decides what a frame
time means, and a recording of fixed-step frames stays smooth however long each
frame took to render.
The hardware converts RGB to YUV, and the stream says which way: limited-range BT.709 primaries, transfer and matrix, written into the H.264 video usability information along with the frame rate.
With bframes above zero the encoder holds pictures back and encode() returns
an empty list until it lets several go at once. Packets then arrive in decode
order carrying display timestamps, and MP4Writer records the difference as
composition offsets. An empty list is an ordinary answer at any setting: never
assume one frame in means one packet out.
Documentation
- docs/usage.md — the recording loop, every encoder setting, timing, colour, reordering, muxing, errors and limits.
- docs/development.md — how the binding works, the ABI harness, and what a new backend has to implement.
- plans/ — the design and its open questions: the whole picture, Intel and AMD, Windows.
Development
pip install -e '.[dev]'
pytest
Tests that need an encoder or a GL context skip themselves without one, so the suite runs anywhere. The hardware tests use a hidden GLFW window and synthetic frames.
The vendor bindings are hand-written ctypes over large C ABIs, and a layout
mistake there corrupts a structure rather than raising anything. So each is
checked against the sizes and offsets its header states —
tests/test_nvenc_abi.py and tests/test_vpl_abi.py — and both run with no
compiler, no driver and no GPU. oneVPL packs each structure to 4 or 8 bytes,
which changes the layout and which a runtime reports only as an invalid
parameter, so those checks earn their keep.
Re-record after changing a structure, or when moving to a newer header:
python tools/record_nvenc_abi.py path/to/nvEncodeAPI.h
python tools/record_vpl_abi.py path/to/libvpl/api/vpl
python tools/record_va_abi.py # /usr/include, from libva-dev
The libva recording carries the value of every constant the binding names as well as the layouts, because an enumerator that moved is as quiet a failure as a field at the wrong offset.
Licence
BSD-3-Clause; see license.txt. It contains no third-party code — see
NOTICES.md for where the NVENC ABI facts come from and under what terms.
Metadata
Release files for pyopengl-video 1.0.0a1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyopengl_video-1.0.0a1.tar.gz | 137.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyopengl_video-1.0.0a1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 237.2 kB
Release files / pyopengl_video-1.0.0a1.tar.gz
| Download URL | pyopengl_video-1.0.0a1.tar.gz |
|---|---|
| Size | 137.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
188c9d6147404ba5e4d72abb3a74918b1b5b51ac342c8e1e011b10e5b7ad5da7
|
|
BLAKE2b-256 checksum How to use checksums |
13fdf65521b3cf67998c763b97a721527a7bd7da79e902a22827f00331682326
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.
Transparency logRelease files / pyopengl_video-1.0.0a1-py3-none-any.whl
| Download URL | pyopengl_video-1.0.0a1-py3-none-any.whl |
|---|---|
| Size | 99.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
96fa52f312e5eba98fa0d045aa318889f95f0a7b1f4b11452876b8afefa49959
|
|
BLAKE2b-256 checksum How to use checksums |
5f2138d99495db0d2e0dc6d4a88ba499f6c474a25f5271f5b5cda6a2235c0ec5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.
Transparency log