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.
There are two ways in, and they are not interchangeable. Open gives a Reader: decode as fast as the hardware allows, in order, with no clock and no sound — for transcoding, analysis, or a renderer that owns its own timing. OpenPlayer gives a Player: AVPlayer's real-time playback, which brings audio, pause, seeking, speed and volume, and which owns the clock itself.
The Reader 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 ¶
- Variables
- type Frame
- type Info
- type OpenError
- type Options
- type PixelFormat
- type Player
- func (p *Player) Close() error
- func (p *Player) CurrentTime() time.Duration
- func (p *Player) Duration() time.Duration
- func (p *Player) Format() PixelFormat
- func (p *Player) Info() Info
- func (p *Player) Pause()
- func (p *Player) Play()
- func (p *Player) Playing() bool
- func (p *Player) Pump(d time.Duration)
- func (p *Player) Rate() float64
- func (p *Player) Seek(at time.Duration) error
- func (p *Player) SetRate(rate float64)
- func (p *Player) SetVolume(v float64)
- func (p *Player) TryFrame() (*Frame, error)
- func (p *Player) Volume() float64
- type Reader
Constants ¶
This section is empty.
Variables ¶
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 or Player is used after its Close. ErrClosed = errors.New("avfoundation: reader or player 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) ToRGBA ¶
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 ¶
OpenError describes a failure to open or start reading a file, carrying whatever AVFoundation said about it.
type Options ¶
type Options struct {
// Format is the pixel format to decode into. Zero means [BGRA].
Format PixelFormat
// AudioDeviceUID names the audio device to play the sound on, as
// coreaudio reports it. Empty -- the default -- lets the system choose,
// which means the default output.
//
// It matters wherever the picture is not on the machine's own screen. A
// film played on a pair of XR glasses draws there and, with nothing named
// here, plays out of the Mac's speakers: no error, no warning, just sound
// coming from the wrong place. The unique id is the stable one -- it
// survives a reboot and a re-plug, and the numeric device ids do not.
//
// A device that does not exist is not an error either: AVFoundation falls
// back to the default output, so a caller that must KNOW where the sound
// went should look the device up first and say so.
AudioDeviceUID string
// ReadyTimeout bounds how long [OpenPlayer] waits for a file to become
// playable before giving up. Zero means ten seconds. [Open] ignores it: an
// AVAssetReader is ready or it is not.
ReadyTimeout time.Duration
}
Options parametrise Open and OpenPlayer. 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 Player ¶ added in v0.2.0
type Player struct {
// contains filtered or unexported fields
}
Player plays a file in real time: with sound, and with a clock that can be paused, moved and run at a different speed.
It is the second of this package's two paths, and the two are not interchangeable. Reader decodes as fast as the hardware will go and hands every frame over in order — the right shape for transcoding, analysis, or a renderer that owns its own clock. Player wraps AVPlayer, which owns the clock itself: it renders audio through the system's output, drops video frames to stay in sync, and answers questions about where in the file it is. Nothing in Reader can do any of that, and nothing here decodes at 2000 frames a second.
Video comes out through Player.TryFrame, which is a POLL, not a pull: AVPlayerItemVideoOutput vends at most one buffer per item time, and asking again for the same instant answers nothing. That is the API AVFoundation offers and it is the right one for a display loop, which draws when the screen is ready and simply wants whatever picture belongs to now.
The main thread, and the run loop ¶
A Player must be opened and driven from the process's MAIN thread, with that thread's run loop running. This is not a style preference; it is measured.
AVFoundation loads a file through the main dispatch queue, and only the main thread's run loop drains that queue. An AVPlayerItem in a program whose main thread is parked never becomes ready: it sits at status 0 for as long as you like, however hard some other thread's run loop is pumped, and on a cold file it reports a duration of 0 with it. Worse, a player in that state still ANSWERS: seek it to ninety seconds and it reports ninety seconds — the value it was just handed. That echo is exactly how a binding that has opened nothing can be mistaken for one that works, and it is what the tests here are built to catch. Sleeping is not waiting.
So: runtime.LockOSThread on the main goroutine, open the player there, and drive it from there. An application already does this — the main thread's run loop IS the window system's event loop, and a Player used from a go-widgets or AppKit program needs nothing extra. A program with no event loop of its own must run one, which is what Player.Pump is for; OpenPlayer runs it for the duration of the load and returns an error rather than a half-loaded Player.
Once loaded, the player keeps going on AVFoundation's own queues. Measured: the clock advances, seeks complete and frames come out with nothing but time.Sleep between the calls. Pump is still what a headless loop should wait with — it costs no more than sleeping and it is where run loop work gets done — but it is not the engine.
A Player is NOT safe for concurrent use.
func OpenPlayer ¶ added in v0.2.0
OpenPlayer opens path for real-time playback and waits for it to become playable, which needs the main thread's run loop and therefore happens here rather than leaving a half-loaded Player in the caller's hands. Call it from the main thread; see Player for why.
The player starts PAUSED at the beginning of the file, at full volume. Call Player.Play to start it.
func (*Player) Close ¶ added in v0.2.0
Close stops playback and releases the player. Frames already handed out stay valid until they are individually released.
func (*Player) CurrentTime ¶ added in v0.2.0
CurrentTime returns where the player is in the file.
It is the player's clock, not a frame's timestamp: while playing it advances continuously, and the frame Player.TryFrame hands back is the one that belongs to it. A closed player reports 0.
func (*Player) Format ¶ added in v0.2.0
func (p *Player) Format() PixelFormat
Format returns the pixel format frames are decoded into.
func (*Player) Pause ¶ added in v0.2.0
func (p *Player) Pause()
Pause stops playback where it is, keeping the position. It is Player.Play's inverse and leaves the rate at 0.
func (*Player) Play ¶ added in v0.2.0
func (p *Player) Play()
Play starts playback at normal speed, with sound.
Like AVPlayer's own play, it sets the rate to 1 — a rate set earlier with Player.SetRate is NOT restored. Use SetRate to resume at another speed.
func (*Player) Playing ¶ added in v0.2.0
Playing reports whether the clock is running, which is exactly whether the rate is not zero.
func (*Player) Pump ¶ added in v0.2.0
Pump runs the calling thread's run loop for about d.
It is what a program with no event loop of its own — a command-line tool, a test — should wait with instead of time.Sleep, and it must be called from the MAIN thread: a run loop can only be run by the thread that owns it, and the main one is the one AVFoundation needs (see above).
An application whose window system already runs the main run loop must NOT call it: running a run loop that is already being run from underneath itself invites reentrancy.
A d of zero or less runs one pass and returns, which is the non-blocking form.
func (*Player) Rate ¶ added in v0.2.0
Rate returns the current playback rate: 0 paused, 1 normal, 2 twice as fast, negative for backwards.
func (*Player) Seek ¶ added in v0.2.0
Seek moves playback to at, accurately: the request goes to AVFoundation with zero tolerance, so it lands on the time asked for rather than on the nearest keyframe. That costs decoding from the previous keyframe forward, which is the trade a viewer wants — measured landing within a microsecond of the request.
The time is clamped to the file: before the start becomes the start, past the end becomes the end. Seeking is asynchronous; the run loop must run (see Player.Pump) before Player.CurrentTime reports the new position.
func (*Player) SetRate ¶ added in v0.2.0
SetRate sets the playback speed. A rate of 0 pauses; 1 is normal speed; 2 is twice as fast; a negative rate plays backwards, which not every file supports — measured working on H.264 in MP4.
A NaN or infinite rate is ignored rather than handed to AVFoundation, which would take it and produce a clock that cannot be reasoned about.
func (*Player) SetVolume ¶ added in v0.2.0
SetVolume sets the audio volume, clamped to the 0..1 AVPlayer accepts. NaN is ignored. This is the player's own volume, not the system's.
func (*Player) TryFrame ¶ added in v0.2.0
TryFrame returns the frame for the player's current time, or (nil, nil) when there is no NEW one to give.
The nil-nil answer is not an error and is the common case: a display loop runs faster than the video's frame rate, and AVPlayerItemVideoOutput vends a buffer only when the picture has changed. A caller draws the last frame again, or nothing.
It is TryFrame rather than NextFrame because there is no "next": the output answers about a moment in time, not about a position in a stream, and after a seek or a rate change the moment can go backwards. A blocking NextFrame would have to either spin or run the run loop behind the caller's back, and both are worse than telling the truth.
Every frame returned must be released, once — see Frame.
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 (*Reader) Close ¶
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.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
avplay
command
Command avplay plays a video file in real time -- with sound -- and reports what actually happened: how many frames the video output vended, how far the clock moved, and how the two compare to the wall clock.
|
Command avplay plays a video file in real time -- with sound -- and reports what actually happened: how many frames the video output vended, how far the clock moved, and how the two compare to the wall clock. |
|
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. |