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 Camera
- type CameraAccess
- type CameraFormat
- type Capture
- type CaptureOptions
- 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.
var ( // ErrNoCamera is returned when there is no video device to open, or when // the one named by [CaptureOptions.Camera] is not attached. ErrNoCamera = errors.New("avfoundation: no such camera") // ErrCameraDenied means the person has said no, in System Settings or at // the prompt. Nothing a program does changes that: the only way back is // Privacy & Security > Camera. ErrCameraDenied = errors.New("avfoundation: the camera was refused by this Mac's owner") // ErrNoUsageDescription means this program has no NSCameraUsageDescription // in its Info.plist. // // ⛔ IT IS NOT A PERMISSION PROBLEM, IT IS A BUILD PROBLEM, and it is the // one that must be caught here: macOS does not deny a program that asks for // the camera without a usage description, it KILLS it -- the process is // terminated by TCC with "This app has crashed because it attempted to // access privacy-sensitive data without a usage description". A library // that let that happen would take its caller down with no error to catch // and nothing in a log. // // The fix is to ship a bundle: see go-macos/appbundle, and give it an // NSCameraUsageDescription saying what the camera is for. A bare binary run // from a terminal has no Info.plist at all. ErrNoUsageDescription = errors.New("avfoundation: this program has no NSCameraUsageDescription; " + "a camera cannot be opened from a bare binary, only from an app bundle") )
Errors a camera can refuse with, which are worth telling apart: two of them are a person's decision and one is a build problem.
Functions ¶
This section is empty.
Types ¶
type Camera ¶ added in v0.6.0
type Camera struct {
// ID is AVFoundation's uniqueID. Pass it to open the device.
ID string
// Name is what the device calls itself, for a person to read. It is often
// NOT distinctive: two different headsets can both report "USB Camera",
// because the name comes from the camera module rather than the product it
// was built into.
Name string
// Model is the manufacturer's model identifier, which on USB carries the
// vendor and product ids — "UVC Camera VendorID_3141 ProductID_25448". It
// is what tells two identically-named devices apart.
Model string
// Formats are the capture shapes the device offers.
Formats []CameraFormat
}
Camera is a video capture device this machine can see.
It is what Cameras returns, and its Camera.ID is what identifies one afterwards. The id is AVFoundation's unique device identifier, which is stable across unplugging and replugging the same device into the same port — unlike the position in the list, which moves whenever anything else is attached.
func Cameras ¶ added in v0.6.0
Cameras reports ErrUnsupported for the same reason.
func (Camera) Best ¶ added in v0.6.0
func (c Camera) Best() (CameraFormat, bool)
Best returns the largest format the camera offers, preferring the highest frame rate among formats of that size, or false when it offers none.
Largest by PIXEL COUNT rather than by width: a camera that offers 1280x1024 alongside 1920x1080 should not have the wider-but-smaller one chosen because its first number is bigger.
type CameraAccess ¶ added in v0.11.0
type CameraAccess int
CameraAccess is what this Mac has already decided about the camera, in the order AVAuthorizationStatus uses.
⭐ ASKING FOR IT DOES NOT PROMPT AND DOES NOT LIGHT THE CAMERA. -[AVCaptureDevice authorizationStatusForMediaType:] reports a decision that has already been made, or not made; it is -requestAccessForMediaType: and starting a session that put a dialog on somebody's screen. That difference is the whole reason this is exported: a program can say "the camera has not been granted" in a start-up report without being the program that asks.
⛔ AND WITHOUT IT, A REFUSED CAMERA IS INDISTINGUISHABLE FROM A MISSING FEATURE. go-xrkit/desk's passthrough did nothing at all, and the desk had no way to say why: it reported the grants it could ask about and left the camera out, on the belief that asking meant opening. It does not.
const ( // CameraNotDetermined means nobody has been asked yet. Opening a camera is // what asks them, and macOS puts its own prompt up: this is not a refusal. CameraNotDetermined CameraAccess = 0 // CameraRestricted means a policy forbids it and the person cannot change // that. Telling them to visit Privacy & Security is the same advice with // one more sentence they cannot act on. CameraRestricted CameraAccess = 1 // CameraDenied means the person has said no, here or in System Settings. CameraDenied CameraAccess = 2 // CameraAuthorized means a camera can be opened with nothing happening // first. CameraAuthorized CameraAccess = 3 )
func CameraAuthorization ¶ added in v0.11.0
func CameraAuthorization() CameraAccess
CameraAuthorization reports CameraNotDetermined away from macOS.
Not "denied": nothing has refused anything. There is no camera to ask about and no decision to report, and calling that a refusal would send a caller looking for a setting to change.
func (CameraAccess) Granted ¶ added in v0.11.0
func (a CameraAccess) Granted() bool
Granted reports whether a camera can be opened with nothing happening first.
⚠ NOT-DETERMINED IS NOT GRANTED, and that is the useful answer for a report: the camera will work, after a prompt somebody has to answer. A report that called that granted would be describing a state that does not exist yet.
func (CameraAccess) String ¶ added in v0.11.0
func (a CameraAccess) String() string
String is what the state is called, in a sentence somebody reads.
type CameraFormat ¶ added in v0.6.0
type CameraFormat struct {
// Width and Height are the picture's dimensions.
Width, Height int
// MinFPS and MaxFPS bound the frame rates this format supports. A device
// that offers one rate reports it as both.
MinFPS, MaxFPS float64
// PixelFormat is what the device delivers, as the same four-character code
// [PixelFormat] already carries for decoded files.
//
// ⚠ It is worth reading rather than assuming. A USB camera commonly offers
// only packed 4:2:2 and no planar format at all: asking such a device for
// a format it does not have does not fail loudly, it simply never
// delivers — which looks exactly like a camera that is not working. That
// is measured, not imagined: ffmpeg asked a headset's camera for yuv420p
// and HUNG rather than refusing.
PixelFormat PixelFormat
}
CameraFormat is one shape a camera can deliver.
func (CameraFormat) String ¶ added in v0.6.0
func (f CameraFormat) String() string
String renders a format the way a person would say it.
type Capture ¶ added in v0.6.0
type Capture struct {
// contains filtered or unexported fields
}
Capture is a running camera.
⚠ THE LIGHT IS ON while it exists. On every Mac with a camera indicator the hardware wires the light to the sensor's power, so it cannot be lit without the camera running and cannot be dark while it is. Close when done, and mean it: a Capture left open is a camera left on.
It is safe to use from several goroutines.
func OpenCamera ¶ added in v0.6.0
func OpenCamera(CaptureOptions) (*Capture, error)
OpenCamera reports ErrUnsupported: a capture session is AVFoundation's, and AVFoundation is macOS's.
func (Capture) Camera ¶ added in v0.6.0
Camera is the device this is running, as Cameras described it.
func (Capture) Close ¶ added in v0.6.0
Close stops the camera and turns its light off. It is safe to call twice.
func (Capture) Latest ¶ added in v0.6.0
Latest is the newest frame the camera has delivered, or false before the first one arrives.
⛔ NEWEST, NOT NEXT. Frames are dropped rather than queued: a camera runs at its own rate and a caller reads at its own, and the two are never the same. A queue between them either grows without bound or blocks the delivery callback, and a blocked callback is a camera that stops. What a viewer wants on screen is what the camera sees now, so an unread frame is worth nothing and is thrown away.
The Frame owns its own memory: it is copied out of the capture buffer before the callback returns, so there is nothing to release and holding one costs only what it is.
type CaptureOptions ¶ added in v0.6.0
type CaptureOptions struct {
// Camera is the [Camera.ID] to open. Empty opens the first one [Cameras]
// lists, which is the Mac's own on a machine with nothing else attached.
Camera string
// Logf receives progress. A nil Logf says nothing.
Logf func(string, ...any)
}
CaptureOptions are the choices a caller makes about a camera.
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.
Source Files
¶
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. |