avfoundation

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 22, 2026 License: BSD-3-Clause Imports: 4 Imported by: 0

README

go-macos/avfoundation

Hardware-accelerated video decoding on macOS from pure Go — CGO_ENABLED=0, via purego.

r, err := avfoundation.Open("movie.mp4")
defer r.Close()

for {
        f, err := r.NextFrame()
        if errors.Is(err, io.EOF) { break }
        // f.Pix aliases the decoder's buffer: Stride bytes per row, BGRA
        use(f)
        f.Release()
}

Why this exists

There is no realistic pure-Go alternative. A software H.264 or HEVC decoder written in Go will not keep up with 4K60; the hardware decoder in every Mac does it without warming up. Measured here on an M4 Max, 720p H.264:

decoded 7740 frames in 3.575s  (2165 fps, 72x real time)

The constraint the fleet actually cares about is no cgo, not "no operating system". This is a system framework reached without a C toolchain.

Design notes

Frames are not copied. A Frame holds the decoder's buffer locked and its pixels stay valid until Release(). At 4K that avoids ~33 MB of copying per frame. Every frame you receive must be released, once — holding many unreleased frames stalls the decoder, which is waiting for its own buffers back.

Use Stride, not Width*4. Decoders pad rows: a 1280-wide frame comes back with a 5120-byte stride here, but that is not guaranteed. Indexing by width shears the picture progressively down the frame, which looks like a decode bug and is not one.

The caller owns the clock. This is a pull API: frames come out as fast as they decode, each carrying its presentation timestamp. That is the right shape for a renderer with its own frame loop — an immersive viewer must draw when the display is ready, not when a player decides.

Two measured limits

BGRA only. Asking for RGBA does not convert — it makes the reader fail (AVAssetReaderStatus 3), and the failure surfaced as an opaque status code from deep inside AVFoundation. Open therefore refuses a non-decodable format up front with a reason, and Frame.ToRGBA converts. The planar YUV formats the hardware natively prefers (NV12 and friends) need a multi-plane Frame this package does not have yet; that is the path to a zero-conversion Metal pipeline.

No Matroska. AVFoundation does not demux MKV or WebM: Open reports ErrNoVideoTrack for both. MP4, MOV and M4V work. For Matroska, demux with go-avkit/avkit/container (which does read EBML, and can recover a track's parameter sets from its samples) and feed the elementary stream to VideoToolbox directly — a separate job from this package.

cmd/avprobe

go run ./cmd/avprobe movie.mp4              # info + first 3 frames as PNG
go run ./cmd/avprobe -n 10 movie.mp4
go run ./cmd/avprobe -all movie.mp4         # decode everything, report the rate

Testing

The portable layer is at 100% statement coverage behind platform seams. The purego bindings need real media, which a CI runner has no business shipping, so they are covered two ways: error paths (absent, junk and empty files, refused formats) run everywhere, and a live decode test runs when AVFOUNDATION_TEST_FILE names a video file:

AVFOUNDATION_TEST_FILE=/path/to/movie.mp4 go test -race ./...

That lane asserts only things checkable from outside the package — the dimensions and frame rate the file itself reports, and that presentation timestamps advance by about one frame period. With it on, the bindings reach ~86%; the rest is failure branches that need a broken decoder rather than a broken file.

Licence: BSD-3-Clause.

Documentation

Overview

Package avfoundation decodes video files on macOS through AVFoundation, with no cgo.

It exists because there is no realistic pure-Go alternative for the job: a software H.264 or HEVC decoder in Go will not keep up with 4K at 60 frames a second, while the hardware decoder in every Mac will do it without warming up. AVFoundation is the system's own front door to that decoder, and it brings demuxing and format support along with it. Everything here goes through github.com/ebitengine/purego, so a consumer still builds with CGO_ENABLED=0 — the constraint the fleet actually cares about is no cgo, not no operating system.

The model is a pull: Open a file and call Reader.NextFrame until it reports io.EOF. Frames come out as fast as they decode, carrying their presentation timestamps, and the caller owns the clock. That is the right shape for a renderer that has its own frame loop — an immersive viewer must draw when the display is ready, not when a player decides.

Frames are NOT copied. A Frame holds the decoder's own buffer locked, and its pixels stay valid until Frame.Release. At 4K that saves about 33 MB of copying per frame, which is the difference between comfortable and not.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnsupported is returned by every entry point on non-darwin platforms.
	ErrUnsupported = errors.New("avfoundation: unsupported on this platform (darwin only)")
	// ErrNoVideoTrack is returned by [Open] for a file with no video in it.
	ErrNoVideoTrack = errors.New("avfoundation: file has no video track")
	// ErrClosed is returned when a Reader is used after [Reader.Close].
	ErrClosed = errors.New("avfoundation: reader is closed")
	// ErrReleased is returned by [Frame] accessors after [Frame.Release].
	ErrReleased = errors.New("avfoundation: frame has been released")
	// ErrUnsupportedFormat is returned by [Open] for a pixel format the decoder
	// will not produce. See [Options.Format].
	ErrUnsupportedFormat = errors.New("avfoundation: the decoder will not produce that pixel format")
)

Errors reported by the package. They are stable and may be tested with errors.Is.

Functions

This section is empty.

Types

type Frame

type Frame struct {
	// Width and Height are this frame's dimensions in pixels.
	Width, Height int
	// Stride is the number of bytes per row, which is USUALLY more than
	// Width*4: the decoder pads rows for alignment. Indexing by Width*4 instead
	// of Stride produces a picture that shears progressively down the frame,
	// which looks like a decode bug and is not one.
	Stride int
	// Format is the pixel layout, as requested when the reader was opened.
	Format PixelFormat
	// PTS is the presentation timestamp: when this frame should be shown,
	// relative to the start of the file.
	PTS time.Duration
	// Pix is the frame's bytes, Stride*Height of them.
	Pix []byte
	// contains filtered or unexported fields
}

Frame is one decoded frame. Its pixels alias the decoder's buffer and are valid until Frame.Release — which the caller must call, once, for every frame it receives. Holding many unreleased frames will stall the decoder, because it is waiting for its own buffers back.

func (*Frame) Release

func (f *Frame) Release()

Release hands the buffer back to the decoder. It is safe to call more than once, and a released Frame's Pix must not be read.

func (*Frame) Released

func (f *Frame) Released() bool

Released reports whether the frame's buffer has been handed back.

func (*Frame) ToRGBA

func (f *Frame) ToRGBA(dst *image.RGBA) *image.RGBA

ToRGBA copies the frame into an *image.RGBA, converting from BGRA if needed.

dst is reused when it is exactly the right size, so a render loop can hold one image and not allocate per frame; pass nil, or an image of the wrong size, to get a fresh one. It returns nil for a released frame.

type Info

type Info struct {
	// Width and Height are the coded dimensions in pixels.
	Width, Height int
	// FrameRate is the track's nominal rate in frames per second. It is nominal:
	// variable-frame-rate material reports an average, so time anything that
	// matters by a frame's own PTS rather than by counting frames.
	FrameRate float64
	// Duration is the track's duration.
	Duration time.Duration
}

Info describes a file's video track, read when it is opened.

type OpenError

type OpenError struct {
	Path   string
	Stage  string
	Detail string
}

OpenError describes a failure to open or start reading a file, carrying whatever AVFoundation said about it.

func (*OpenError) Error

func (e *OpenError) Error() string

type Options

type Options struct {
	// Format is the pixel format to decode into. Zero means [BGRA].
	Format PixelFormat
}

Options parametrise Open. The zero value asks for BGRA, which is what the decoder produces natively.

type PixelFormat

type PixelFormat uint32

PixelFormat is a CoreVideo pixel format, which is a four-character code.

const (
	// BGRA is 32-bit BGRA, 8 bits per channel. It is what the display pipeline
	// and Metal both prefer, so it is the default: asking for RGBA instead would
	// make something, somewhere, swap two bytes per pixel for nothing.
	BGRA PixelFormat = 0x42475241 // 'BGRA'
	// RGBA is 32-bit RGBA. It DESCRIBES a frame's layout but is NOT accepted as a
	// decode request: measured on macOS, an AVAssetReaderTrackOutput asked for
	// RGBA fails outright (reader status 3) rather than converting. Use
	// [Frame.ToRGBA] to convert a decoded BGRA frame instead.
	RGBA PixelFormat = 0x52474241 // 'RGBA'
)

The formats this package can ask the decoder for.

func (PixelFormat) String

func (f PixelFormat) String() string

String renders the format as its four-character code.

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

Reader decodes a file's first video track, in order, from the beginning.

It is NOT safe for concurrent use: one goroutine pulls frames. Seeking is not implemented yet — a reader plays through once.

func Open

func Open(path string, opts ...Options) (*Reader, error)

Open opens path and prepares its first video track for decoding.

func (*Reader) Close

func (r *Reader) Close() error

Close releases the decoder. Frames already handed out stay valid until they are individually released.

func (*Reader) Format

func (r *Reader) Format() PixelFormat

Format returns the pixel format frames are decoded into.

func (*Reader) Info

func (r *Reader) Info() Info

Info returns the video track's description.

func (*Reader) NextFrame

func (r *Reader) NextFrame() (*Frame, error)

NextFrame decodes and returns the next frame, or io.EOF when the track is exhausted. The caller must Frame.Release every frame it receives.

Directories

Path Synopsis
cmd
avprobe command
Command avprobe decodes the first frames of a video file and writes them as PNGs.
Command avprobe decodes the first frames of a video file and writes them as PNGs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL