AV1 video frames
Av1StreamDecoder decodes AV1 video a temporal unit at a time and Av1StreamEncoder encodes it; both work on frames, and
Egami.Video uses them for its containers.
Decoding
Av1StreamDecoder decodes AV1 video a temporal unit at a time: key, inter, intra-only and switch frames with every inter tool of
the Main, High and Professional profiles (single and compound references, OBMC, local and global warps, wedge, difference-weighted
and distance-weighted compound, inter-intra), 8, 10 and 12 bits, 4:0:0 to 4:4:4, film grain, frame size changes with scaled
references, superres and scalable streams. Every frame equals libaom's sample for sample, on libaom's test vectors and on clips made
with its encoder for each tool. It reads temporal units, not files: WebM, Matroska, MP4 and IVF are read by Egami.Video, which uses
this decoder, and units from any other source decode the same way.
// AV1 temporal units in stream order, here from an IVF file (Egami.Video reads WebM, Matroska, MP4 and IVF for you).
using Av1StreamDecoder decoder = new(new Av1DecoderOptions { ApplyFilmGrain = true });
foreach (byte[] temporalUnit in IvfFrames("clip-av1.ivf"))
{
decoder.Send(temporalUnit);
while (decoder.NextFrame is Av1FrameInfo next)
{
// A unit can show no frame (a hidden reference), one, or one per spatial layer.
using YuvRaster picture = next.BitDepth == 8
? YuvRaster.Create(next.Width, next.Height, next.Chroma, YuvColor.Bt601Limited)
: YuvRaster.Create(next.Width, next.Height, next.Chroma, next.BitDepth, YuvColor.Bt601Limited);
decoder.TryReceiveFrame(picture, out Av1FrameInfo info); // the picture is the caller's: change it or keep it
shown++;
}
}
Send takes one temporal unit: the low-overhead OBUs a container sample holds, each with its size. A unit can show no frame (a
hidden frame, kept only as a reference), one frame, or with OutputAllLayers one per spatial layer. NextFrame gives the size,
bit depth and chroma format of the next frame, and TryReceiveFrame copies it into a YuvRaster of that shape: 8-bit samples for
an 8-bit stream, 16-bit samples holding 10 or 12 bits otherwise. The copy belongs to the caller, so it can be changed or kept while
later units decode. A frame shown again with show_existing_frame comes out without decoding.
Av1FrameInfo says what the frame was: its type (Av1FrameKind), whether it is a key frame shown in its own unit (a point a player can start
from), its size and render size, bit depth, chroma format, temporal and spatial layer, and the sequence header's color code points.
LastHeaders lists the fields of the last unit's sequence and frame headers under the specification's names.
Options
Av1DecoderOptions chooses what comes out. ApplyFilmGrain (on by default) adds the stream's film grain to the frames; the
references keep the frames without grain either way. OperatingPoint picks the layers of a scalable stream that decode (0 by
default), and OutputAllLayers gives every spatial layer of a unit instead of only the highest.
Broken data and limits
Decoding starts at a key frame. A broken unit, or one that refers to a frame the decoder does not have, throws
CorruptDataException, and the decoder carries on from the next unit it is given; Reset drops the references so the next unit
must start a sequence again. A frame larger than the context's SafetyLimits throws LimitExceededException.
Memory and threads
The decoder keeps the eight reference frames and the frame being decoded. A frame's tiles decode on up to
EgamiContext.MaxParallelism threads, and its loop filter, CDEF and loop restoration run in rows on the same threads; the result
does not depend on the thread count.
Encoding
Av1StreamEncoder encodes YuvRaster pictures of one size into AV1 frames: key frames and intra-only frames with the AVIF
encoder's intra search, and inter frames that predict each block from up to seven references. The motion search starts from the
vectors the neighbours and the reference frames give, searches whole pixels and then refines to a quarter or an eighth of a pixel,
and every block weighs NEARESTMV, NEARMV, GLOBALMV and NEWMV, the compound pairs and the intra modes by rate and distortion.
Higher efforts add distance-weighted, wedge and difference-weighted compounds, inter-intra, overlapped and locally warped motion,
dual interpolation filters and each reference's global motion. libaom and dav1d decode every frame exactly as Av1StreamDecoder
does.
// Pictures of one size, here decoded from a clip; Egami.Video writes WebM, MP4 and IVF and plans the frames for you.
using Av1StreamEncoder encoder = new(new Av1SequenceOptions { Width = pictures[0].Width, Height = pictures[0].Height });
units.Add(encoder.Encode(pictures[0], new Av1FramePlan { Kind = Av1FrameKind.Key, Quality = 60 }).Data.ToArray());
// A hidden frame from four pictures ahead goes into slot 1; the three pictures between read it as ALTREF (from the future)
// and the frame before as LAST, then the hidden frame is shown when its time comes.
Av1FramePlan between = new() { Quality = 60, ReferenceSlots = [0, 0, 0, 0, 1, 1, 1] };
encoder.Encode(pictures[4], new Av1FramePlan { Quality = 55, Show = false, OrderHint = 4, RefreshSlots = 0b10 });
for (int i = 1; i < 4; i++)
{
Av1EncodedFrame frame = encoder.Encode(pictures[i], between); // the first unit also holds the hidden frame
units.Add(frame.Data.ToArray()); // an MP4 sample or a Matroska block; frame.LumaPsnr says how close it came
}
units.Add(encoder.ShowExisting(1).Data.ToArray());
Av1SequenceOptions describes the stream: its size, bit depth (8, 10 or 12), chroma format, color code points, chroma siting and
level; the encoder picks the Main, High or Professional profile they need. Av1SequenceOptions.FromSequenceHeader takes the
sequence header of an existing stream instead, and the encoder then writes that header byte for byte, so its frames can join the
stream, as a frame-exact cut needs. SequenceHeader is the OBU an MP4 or Matroska av1C box holds.
Each call takes an Av1FramePlan, so the caller decides what the stream looks like:
Kindmakes a key frame, an intra-only frame or an inter frame; the first frame is always a key frame.Quality(0 to 100, AVIF's scale) orQIndex(base_q_idx, 0 to 255, for rate control; 0 is lossless) sets the frame's quantizer, andEffort(0 to 10, default 4) how hard the encoder searches.ReferenceSlotsnames the slot each of LAST to ALTREF reads,References(anAv1References) which of them blocks may use, andRefreshSlotswhich slots the frame replaces. References that read the same frame are searched once.Show = falsecodes a hidden frame, such as an alternate reference from frames to come;OrderHintsays where it shows, andShowExistingshows it later with show_existing_frame.TuneisAv1Tune.Video(the default: one quantizer for luma and chroma, no quantization matrices, rate-distortion weights by frame type) orAv1Tune.Image, the still-picture tools AVIF uses.Layeris the frame's layer in its group's pyramid: the deeper it is, the fewer frames predict from it and the fewer bits it spends.Referencedsays whether any later frame does.ErrorResilientmakes a frame that decodes without the probabilities and vectors of the frames before it,PrimaryReferencepicks the reference a frame takes them from,DeltaQuantizersturns the per-superblock quantizers off,VarianceBoostgives flat superblocks finer ones, andScreenContentthe palettes on or off.
Encode returns an Av1EncodedFrame with the temporal unit's OBUs, without temporal delimiters, as MP4 samples and Matroska blocks
store them; a key frame's unit starts with the sequence header. A hidden frame's data is empty: its OBUs lead the next shown frame's
unit, so every unit has one shown frame. The result also gives the frame's bits, quantizer, order hint and the squared error of
the decoded frame against the picture (LumaPsnr from it), what rate control needs to plan the next frame.
The encoder keeps the eight reference slots as a decoder does, and decides a frame's tiles on up to EgamiContext.MaxParallelism
threads; its output does not depend on the thread count. AVIF stills are not affected: they encode as before, byte for byte.
Temporal analysis
Before coding a group, Analyze can look at its frames and the group after it in coding order, each naming the frames it will
predict from. For every 16x16 block it measures what predicting from the references saves against coding the block alone, and
carries that back from the last frame to the first: a block the frames after it reuse a lot carries much. Importance sums it
over the frame, so a group's key frame and alternate reference can take finer quantizers the more they are reused, and a plan's
Analysis gives a referenced layer-0 frame finer quantizers in the superblocks that matter most and the other frames lower
rate-distortion weights there. The analysis reads the pictures only and is the same at any thread count.
// The frames ahead in coding order: a key frame, a hidden frame four pictures on, then the three between, each predicting
// from the frames on both sides of it (their places in this list).
IReadOnlyList<Av1FrameAnalysis> analysis = encoder.Analyze(
[
new Av1AnalysisFrame { Picture = pictures[0] },
new Av1AnalysisFrame { Picture = pictures[4], References = [0] },
new Av1AnalysisFrame { Picture = pictures[1], References = [0, 1] },
new Av1AnalysisFrame { Picture = pictures[2], References = [0, 1] },
new Av1AnalysisFrame { Picture = pictures[3], References = [0, 1] },
]);
// The more the frames after them reuse the key and hidden frames, the finer their steps; each plan carries its analysis.
int keyQ = (int)Math.Round(100 / Math.Sqrt(analysis[0].Importance));
encoder.Encode(pictures[0], new Av1FramePlan { Kind = Av1FrameKind.Key, QIndex = keyQ, Analysis = analysis[0] });
encoder.Encode(pictures[4], new Av1FramePlan { QIndex = 110, Show = false, OrderHint = 4, RefreshSlots = 0b10, Analysis = analysis[1] });
for (int i = 1; i < 4; i++)
{
encoder.Encode(pictures[i], new Av1FramePlan { QIndex = 140, Layer = 1, ReferenceSlots = [0, 0, 0, 0, 1, 1, 1], RefreshSlots = 0, Analysis = analysis[i + 1] });
}
encoder.ShowExisting(1);