Table of Contents

Lossless archiving with FFV1

FFV1 (RFC 9043) is the lossless video codec memory institutions archive with: a decode gives back every sample, each slice of every frame carries a CRC, and every frame can stand on its own. Egami.Video reads and writes it in Matroska and AVI with no native code, frame for frame equal to FFmpeg's decoder, and FFmpeg decodes Egami.Video's files to the same samples.

Writing a master

Pick VideoCodecs.Ffv1 and write frames as you would with any codec. Planes keep their layout and depth (4:2:0, 4:2:2, 4:4:4, 4:1:1, 4:1:0 or gray, with or without alpha, 8 to 16 bits), so a 10-bit 4:2:2 capture stays 10-bit 4:2:2. Write Matroska (.mkv), the usual archival pairing; AVI works too.

// An uncompressed 10-bit 4:4:4 capture into an FFV1 master: the planes keep their layout and depth, and the defaults are
// the archival profile (version 3, every frame a keyframe, a CRC on every slice).
using (VideoReader capture = VideoReader.Open("capture.y4m"))
using (VideoWriter master = VideoWriter.Create("master.mkv", new VideoWriterOptions
{
    Codec = VideoCodecs.Ffv1,
    FrameRate = capture.Track.FrameRate ?? new Rational(25, 1),
}))
{
    foreach (YuvVideoFrame frame in capture.ReadFrames())
    {
        using (frame)
        {
            master.WriteFrame(frame);
        }
    }
}

The defaults are the profile archives use, which FFmpeg users get by adding -level 3 -g 1 -slicecrc 1 -slices 16:

Setting Default FFmpeg flag
Version 3, with a configuration record in the container -level 3
Keyframes every frame (KeyframeInterval 1) -g 1
Slice CRCs on (Ffv1EncoderOptions.SliceCrc) -slicecrc 1
Slices 1 up to 352×288, 4 up to 720p, 16 up to 1440p, 24 above -slices
Coder range coder with a custom state table -coder range_tab
Context model small (Effort up to 6) -context 0

Files come within 1 % of FFmpeg's size at the same settings, and slices are coded in parallel on EgamiContext.MaxParallelism threads with the same file at any thread count.

Checking fixity

Ffv1Codec.Verify checks the configuration record's CRC and every slice's without decoding a picture, about 50 times faster than a decode, so it suits routine checks of a whole collection. Ffv1VerifyResult.Errors names each damaged frame and slice. A decode with Errors = DecodeErrorMode.Conceal reports the same damage as Ffv1.SliceCrc and shows the last good frame in the damaged slice's place. After a migration, a full check decodes the master and compares it with the source:

// A routine fixity check: every slice's CRC, without decoding a picture.
Ffv1VerifyResult check = Ffv1Codec.Verify("master.mkv");
Console.WriteLine($"{check.Frames} frames, {check.Slices} slices: {(check.IsIntact ? "intact" : $"{check.Errors.Count} damaged")}");

// A full check after migration: decode the master and compare every sample with the source.
using VideoReader source = VideoReader.Open("capture.y4m");
using VideoReader master = VideoReader.Open("master.mkv");
int frames = 0, different = 0;
foreach (YuvVideoFrame original in source.ReadFrames())
{
    using (original)
    using (YuvVideoFrame copy = master.ReadFrame()!)
    {
        frames++;
        different += SamePlanes(original.Raster, copy.Raster) ? 0 : 1;
    }
}

Console.WriteLine($"{frames} frames compared, {different} differ");

Version 0 and 1 files, and version 3 files written without slice CRCs, have nothing to check per slice: HasSliceCrcs is false.

RGB sources

Scanned film, graphics and screen captures in RGB stay RGB: frames given as RGB rasters (8 or 16 bits a channel) are coded through FFV1's reversible color transform, and decode as G, B and R planes tagged YuvMatrix.Identity, which convert back to RGB exactly.

// Scanned film or graphics in RGB stays RGB: frames given as RGB rasters are coded through FFV1's reversible color
// transform and come back exactly, here as 16-bit RGB for a restoration tool.
using (VideoWriter writer = VideoWriter.Create("scan.mkv", new VideoWriterOptions { Codec = VideoCodecs.Ffv1, FrameRate = new Rational(24, 1) }))
{
    writer.WriteFrame(duck);
}

using VideoReader reader = VideoReader.Open("scan.mkv");
using VideoFrame<Rgb16> restored = reader.ReadFrames<Rgb16>().First();

Planes decoded from a 9, 11, 13, 14 or 15-bit source sit in 10, 12 or 16-bit planes with their samples shifted up; set Ffv1EncoderOptions.BitDepth to write them back at the source depth. The track's BitDepth gives the depth a file holds.

Working copies

For copies made to work on rather than to keep, Golomb-Rice coding is faster than the range coder, spacing keyframes makes files smaller, and more slices use more cores. Quality does not apply to FFV1, and setting it reports Ffv1.QualityIgnored.

// Smaller and faster masters for a working copy: Golomb-Rice coding, a keyframe every second, more slices for more cores.
// Slice CRCs stay on, so Ffv1Codec.Verify still checks every frame.
VideoWriterOptions mezzanine = new()
{
    Codec = VideoCodecs.Ffv1,
    FrameRate = new Rational(25, 1),
    KeyframeInterval = 25,
    Ffv1 = new Ffv1EncoderOptions { Coder = Ffv1Coder.GolombRice, Slices = 24 },
};