Apple hardware encoding: VideoToolbox

Chapter 4: Apple·about 1135 words

Creating and configuring the session

Creating and configuring the session, and the three input paths

The implementation registers into the encoder registry by codec and is marked as hardware; taking it by the default name from the upper layer gets this one. Before creating the session, the unified codec identifier is translated into the type constant it recognizes, then the session is created with the resolution, and the callback is attached at this point too — again carrying its own pointer in as an opaque argument (the same treatment appears in the hardware-decode integration article).

Six properties are configured: real-time mode on, profile (baseline for the low-compression codec, main for the high-compression one), frame reordering disallowed (equivalent to disabling bidirectional predicted frames), average bitrate, expected frame rate, and maximum keyframe interval — the last computed as keyframe interval times frame rate, with the base class default used when the upper layer supplies no interval.

The bitrate is not supplied by hand: computing it belongs to the base class, which derives one from resolution and frame rate when the upper layer does not set it, the same algorithm as on the other two ends, and this implementation only takes the result. Rate control likewise only gets the average bitrate; keys such as constant bitrate or a quality tier are not set at all, and how the platform weighs things internally is not second-guessed here.

There are two version gates: too old a system does not support hardware encoding, and a slightly older one does not support the high-compression codec; both are reported as “unsupported” and left to the upper layer to switch candidates.

After the properties are set, “prepare to encode” must be declared explicitly — frames sent before this step are not accepted; sending starts only after the declaration.

Three input paths

The encoder has three frame-input overloads, corresponding to three kinds of frames; which one is used is decided by the type the frame carries, with no prior declaration from the caller. That way the same encoder can handle frames coming from different places on the recording chain:

When the frame carries that shared surface (the result processed by the pipeline), a pixel buffer is wrapped around the surface and then sent in. This path is also the youngest of the three: the attributes passed when wrapping the buffer are empty, no expected input format is declared to the encoder, and whether every device model and resolution accepts this conversion cannot be told from the code and has not been verified on real devices.

When the frame is directly a system pixel buffer (the camera-direct path, see the capture article), it is sent as is — no extra copy.

When the frame holds planar data in memory, a pixel buffer is created, the planes are copied in, and it is released after sending.

On the “through Vulkan” path, what is shared out is a BGRA surface, not a Metal texture; the encoder itself knows nothing about Vulkan, it just gets a piece of graphics memory that can be wrapped as a pixel buffer. This is also the surface described in the two destinations for hardware-decoded frames — used in the opposite direction.

The output: bitstream, timestamps, keyframes

How the output is taken: timestamps, keyframes and that one resent config frame

The callback does four things:

Take the bitstream. Get the pointer and length from the sample's data block, assemble a bitstream packet, with the prefix counted as a four-byte start code.

Compute the timestamp. Convert the system-provided presentation time to milliseconds: first to seconds (this step uses floating point), then multiply by one thousand. There was a pitfall here: earlier the integer part of this value was divided directly by the timescale, so the sub-second part was truncated, and since a frame is only tens of milliseconds, every timestamp in the whole film came out as zero. This value still gets zeroed and converted to the stream's own time base at the muxer; this layer only has to avoid losing precision. Decode and encode take it the same way.

Check for dropped frames. When the flags given by the callback carry drop information, return directly without going further.

Check for a keyframe. Look at whether the sample's attachments carry that “not sync” key: if present it is an ordinary frame, if absent it is a keyframe.

Config frames: the callback carries no parameter sets, so they have to be supplied here

This API's encode callback carries no parameter sets, so config frames must be supplied here: at the keyframe moment, the parameter sets are taken from the format description by index — two for the low-compression codec, three for the high-compression one, in a fixed order that the muxer reads in. Each is prefixed with a four-byte length header, a long integer in network byte order.

The resend is pinned to the keyframe moment and never happens on ordinary frames; if a decoder mid-stream loses the config frames, it has to wait for the next keyframe. This segment cannot be skipped: the muxer waits for config frames before creating the stream, and without them the file header written out is empty. One more thing has to line up: the bitstream here is length-prefixed, not start-code form — that is what the encoder produces, and the sets supplied here must match it, or the next reader will slice by the prefix and cut at the wrong boundaries.

Closing the stream: drop late callbacks first, then drain the frames in flight

Closing the stream sets a flag first, after which late encode callbacks are dropped outright; before closing, the session is asked to drain the frames in flight once, so no callback is still using those resources when the session is released.

Shared with the decode side: parameter-set tools and version probing

Each side creates its own session, with no session object shared; what is really shared is the same parameter-set handling tools and system version probing. This is consistent with the other two platforms — hardware decode and hardware encode share the device and texture layer, not the codec objects.

The bitstream packets handed out here go into the same muxing path (the recording-chain article), and the config-frame packets decide when the muxer can create the stream; the timestamp convention has to stay consistent with other sources on the chain — frames on the capture side use the local clock, this side uses the system-provided presentation time, and when the two sources are mixed into one stream, the first-packet zeroing step takes the first packet's decode timestamp, so the offset is not amplified, but it is not eliminated either.

What is shared with the decode side is the parameter-set tools and version probing; what is shared with the two destinations for hardware-decoded frames is that surface.

The avox engine is open source on GitHub: github.com/xxxzhou/avox, where this compression session's configuration and the three input paths all live.