Table of Contents

Video

Egami.Video reads and writes video with Egami rasters as frames, so resizing, color and drawing from Egami work on video with no copying. It is fully managed and ships only royalty-free codecs:

  • Containers: WebM and Matroska, AVI (with OpenDML for files over 1 GB), IVF and YUV4MPEG2, read and written, and headerless raw frames.
  • Codecs: AV1 (8, 10 and 12 bit, 4:0:0 to 4:4:4, film grain, equal to libaom frame for frame), VP9 (all four profiles, 8, 10 and 12 bit, equal to libvpx frame for frame), VP8 (with WebM alpha) and Motion JPEG decode; VP8 (see Encoding video), Motion JPEG and uncompressed encode. Audio codecs such as FLAC, Opus, ALAC and AC-3 (see Audio); other tracks are copied through untouched. H.264 decodes with the opt-in Egami.Video.H264 package (see H.264).

Probing a file

MediaFile.Probe reads the headers and returns a MediaInfo: the container's FormatName, the Duration, and the Tracks. A TrackInfo gives a track's Number, Kind (a TrackKind) and CodecId (see CodecIds); a VideoTrackInfo adds its size, bit depth, chroma format, FieldOrder, frame rate, color and HDR metadata, and an AudioTrackInfo its sample rate and channels. MediaMetadata holds the tags, the chapters (ChapterInfo) and the attachments (AttachmentInfo).

// What a file holds, without decoding: its container, duration, tracks, tags, chapters and attachments.
MediaInfo info = MediaFile.Probe("metadata.mkv");
Console.WriteLine($"{info.FormatName}, {info.Duration?.ToTimeSpan()}");
foreach (TrackInfo track in info.Tracks)
{
    string details = track switch
    {
        VideoTrackInfo v => $"{v.Width}x{v.Height} {v.BitDepth}-bit {v.ChromaFormat}, {v.FieldOrder}",
        AudioTrackInfo a => $"{a.SampleRate} Hz, {a.Channels} channels",
        _ => "",
    };
    Console.WriteLine($"#{track.Number} {track.Kind} {track.CodecId} {details}");
}

MediaMetadata metadata = info.Metadata;
foreach (ChapterInfo chapter in metadata.Chapters)
{
    Console.WriteLine($"{chapter.Start.ToTimeSpan()} {chapter.Title}");
}

foreach (AttachmentInfo attachment in metadata.Attachments)
{
    File.WriteAllBytes(attachment.Name, attachment.Data.ToArray());
}

Reading frames

VideoReader opens a file and decodes its first video track. ReadFrames<TPixel> gives VideoFrame<TPixel> frames of RGB pixels, each with its timestamp, duration and color; dispose each frame when done with it.

// Every frame as RGB pixels: VP9, VP8, Motion JPEG or raw, from WebM, Matroska, AVI, IVF or YUV4MPEG2.
using VideoReader reader = VideoReader.Open("movie.webm");
Console.WriteLine($"{reader.Track.Width}x{reader.Track.Height} at {reader.Track.FrameRate?.ToDouble()} fps");
foreach (VideoFrame<Rgba8> frame in reader.ReadFrames<Rgba8>())
{
    using (frame)
    {
        frame.Raster.Save($"frame-{frame.Timestamp.ToTimeSpan().TotalMilliseconds:0000}.png");
    }
}

ReadFrame and ReadFrames without a pixel type give a YuvVideoFrame: the codec's own planes as a YuvRaster, at the stream's subsampling and bit depth, with no conversion. Both kinds of frame are a VideoFrame, which is what encoders and writers take.

// The codec's own planes, with no conversion: luma and chroma at the stream's subsampling and bit depth.
using VideoReader reader = VideoReader.Open("movie.webm");
using YuvVideoFrame first = reader.ReadFrame()!;
YuvRaster planes = first.Raster;
Console.WriteLine($"{planes.ChromaFormat} {planes.BitDepth}-bit, color {first.Color}");
VideoFrame frame = first;                     // the base type encoders and writers take
using Raster<Rgb8> rgb = first.ToRaster<Rgb8>();

VideoReaderOptions picks the track, the codec's VideoDecoderOptions (such as DecodeAlpha for WebM alpha), and what a broken frame does, as a DecodeErrorMode: Skip (the default) reports it and drops frames up to the next keyframe, Conceal repeats the last good frame, and Throw throws.

// How a reader decodes: which track, what a broken frame does (Skip, the default, drops it up to the next keyframe;
// Conceal repeats the last good frame; Throw throws) and the codec's own options.
VideoReaderOptions options = new()
{
    TrackNumber = 1,
    Errors = DecodeErrorMode.Conceal,
    Decoder = new VideoDecoderOptions { DecodeAlpha = false },
};
using VideoReader reader = VideoReader.Open("alpha.webm", options);
using YuvVideoFrame frame = reader.ReadFrame()!;
// Opening reads the headers; the async forms do that off the calling thread.
using VideoReader reader = await VideoReader.OpenAsync("movie.webm", VideoReaderOptions.Default);
using YuvVideoFrame? frame = reader.ReadFrame();

Seeking

Seek takes a MediaTime and lands on the frame shown at that time, decoding from the keyframe before it; it is exact over VP9's hidden and show-existing frames too. SeekMode.Keyframe stops at the keyframe instead, which is faster.

// An exact seek lands on the frame shown at the time; SeekMode.Keyframe stops at the keyframe before it, which is faster.
using VideoReader reader = VideoReader.Open("movie.webm");
reader.Seek(MediaTime.FromSeconds(0.5));
using YuvVideoFrame atHalf = reader.ReadFrame()!;
reader.Seek(MediaTime.FromSeconds(0.5), SeekMode.Keyframe);
using YuvVideoFrame keyframe = reader.ReadFrame()!;

Writing a clip

VideoWriter encodes images or frames into a file or stream; VideoWriterOptions sets the codec, the frame rate (a Rational), the quality, the chroma format, the metadata and other tracks to copy in.

// Images into a Motion JPEG clip; the container comes from the extension.
VideoWriterOptions options = new() { Codec = VideoCodecs.MotionJpeg, FrameRate = new Rational(25, 1), Quality = 85 };
using (VideoWriter writer = VideoWriter.Create("duck.mkv", options))
{
    for (int i = 0; i < 10; i++)
    {
        writer.WriteFrame(duck);
    }
}

// Or into a stream, in a format by name.
using MemoryStream avi = new();
using (VideoWriter writer = VideoWriter.Create(avi, "avi", options, leaveOpen: true))
{
    writer.WriteFrame(duck);
}

Remuxing

MediaFile.Remux copies a file's packets into another container without decoding. For more control, an IDemuxer reads EncodedPackets (data, timestamps, the keyframe flag and PacketAdditions such as WebM alpha) and an IMuxer writes them.

// Copy the packets into another container without decoding; the sound and the tags come along.
long packets = MediaFile.Remux("camera.avi", "camera.mkv");

// Or packet by packet, choosing what goes through.
using IDemuxer demuxer = MediaFile.OpenDemuxer("movie.webm");
using (IMuxer muxer = MediaFile.CreateMuxer("movie.ivf", [demuxer.Info.VideoTrack!]))
{
    while (demuxer.ReadPacket() is { } packet)
    {
        using (packet)
        {
            // Data, the time and whether it starts a keyframe; WebM alpha travels in Additions.
            IReadOnlyList<PacketAddition> additions = packet.Additions;
            muxer.WritePacket(packet);
        }
    }

    muxer.Finish();
}

Trimming

MediaFile.Trim cuts the part of a file from a start up to an end into a new file whose timestamps start at 0. The cut's first frame is the first at or after the start, and it ends after the last frame that starts before the end; a time within half a tick of a frame, such as the millisecond Matroska keeps, counts as that frame's. TrimMode says what happens when the start falls between keyframes:

  • TrimMode.Smart, the default, is frame-exact and encodes as little as it can. It decodes from the keyframe before the start and encodes the frames from the start up to the next keyframe again, as a new group that starts with a keyframe; every packet from that keyframe on is copied byte for byte. The new group matches the stream (codec, profile, size, bit depth, chroma format and color, and for AV1 the sequence header byte for byte, as MP4 and Matroska keep it in the track header) and is coded at the average quantizer of the packets it replaces, so the quality does not jump. A start on a keyframe, and Motion JPEG or uncompressed video, are a plain copy.
  • TrimMode.Keyframe decodes nothing: the cut starts at the keyframe at or before the start, as ffmpeg -ss -c copy does.
  • TrimMode.Reencode encodes every frame of the cut again, with the same codec at the source's quantizer.

The end needs no encoding: VP8, VP9 and AV1 decode in packet order and code a hidden frame before the frames that show it, so the cut keeps every packet up to the one that shows its last frame and drops a hidden frame after it that nothing shows.

Smart cut falls back where it must and says so in a MediaWarning, in TrimResult.Warnings and through MediaContext.Warnings: a codec Egami.Video cannot encode (H.264, HEVC) starts at the keyframe (trim-keyframe), as do frames that change size, and a stream the encoder cannot match (a VP8 version other than 0, 4:4:0 or monochrome AV1, an AV1 sequence header with frame ids or a decoder model) is encoded again whole (trim-reencode). TrimResult tells where the cut starts in the input, how many frames it holds and how many were encoded again.

Audio and subtitles are copied. Audio is cut at the sample where the format can say so: PCM by slicing its packets, MP4 by its edit list, and Opus in Matroska and WebM by CodecDelay (with 80 ms of pre-roll) and DiscardPadding. Other audio is cut at the packet whose middle is nearest (trim-audio-packet). Subtitle cues shown across the cut are clipped to it, and so are chapters; tags and attachments are kept. A seekable input is read from a few seconds before the start. Trim takes a Stream in and out, progress and a token as the other jobs do.

// Frame-exact: only the frames from 0.3 s up to the next keyframe are encoded again; every other packet is copied.
TrimResult result = MediaFile.Trim("show.webm", "cut.webm", MediaTime.FromSeconds(0.3), MediaTime.FromSeconds(0.8));
Console.WriteLine($"{result.Frames} frames from {result.Start.Seconds:0.000} s, {result.ReencodedFrames} encoded again");
foreach (MediaWarning warning in result.Warnings)
{
    Console.WriteLine(warning.Message);
}

// No decoding at all: the cut starts at the keyframe at or before 0.3 s.
MediaFile.Trim("show.webm", "copy.webm", MediaTime.FromSeconds(0.3), MediaTime.FromSeconds(0.8), TrimMode.Keyframe);
// Streams in and out, such as cloud blobs: a non-seekable input is read from its start.
using Stream source = File.OpenRead("movie.webm");
using Stream upload = File.Create("cut.mkv");
IProgress<MediaProgress> progress = new Progress<MediaProgress>(p => Console.Write($"\r{p.Fraction ?? 0:P0}"));
TrimResult result = MediaFile.Trim(source, upload, "mkv", MediaTime.FromSeconds(1), null, TrimMode.Smart,
    tracks: t => t.Kind == TrackKind.Video, leaveOpen: true, progress: progress, cancellationToken: cts.Token);

On the command line, egami-video trim --smart cuts this way, and segment --scenes --exact starts every part on its scene.

Progress, cancellation and streams

MediaFile.Remux and MediaFile.Transcode take an IProgress<MediaProgress> and a CancellationToken as their last parameters. A MediaProgress report carries the position, the duration, a 0 to 1 Fraction, frames, packets, bytes read and written, the elapsed time and the speed (media seconds per second, like FFmpeg's speed=). Reports come once the output is open, then at most every 250 ms, then once more with IsFinal set and Fraction 1. The fraction comes from the time when the input has a duration, else from the bytes read over the input's length, else it is null; it never goes backwards. The handler runs on the job's thread; Progress<T> moves it to the UI thread. If the handler throws, the job stops with that exception. Without a handler, nothing is built or timed.

// Reports arrive once the output is open, then at most every 250 ms, then once more with IsFinal set.
// Progress<T> runs the handler on the caller's synchronization context: the UI thread in WinForms, WPF or MAUI.
IProgress<MediaProgress> progress = new Progress<MediaProgress>(p =>
    Console.Write($"\r{p.Position.ToTimeSpan():c} {p.Fraction ?? 0:P0} {p.Frames} frames {p.Speed:0.0}x"));
long packets = MediaFile.Remux("camera.avi", "camera.mkv", progress: progress);

The token is checked before each packet. A cancelled job throws OperationCanceledException and does not finish the output: a file the job created is deleted, and a stream you passed in keeps what was written so far.

// Ctrl+C stops the job between two packets; the half-written camera.mkv is deleted.
using CancellationTokenSource cts = new();
ConsoleCancelEventHandler onCtrlC = (_, e) =>
{
    e.Cancel = true;
    cts.Cancel();
};
Console.CancelKeyPress += onCtrlC;
try
{
    MediaFile.Remux("camera.avi", "camera.mkv", cancellationToken: cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Cancelled.");
}
finally
{
    Console.CancelKeyPress -= onCtrlC;
}

Both jobs also take a Stream in and a Stream out, with the output container named, so they can read from and write to cloud storage without a local copy. The input may be non-seekable; only MP4 files with moov after the media need a seekable one. Matroska, WebM, IVF and YUV4MPEG2 write to a non-seekable stream as they go, MP4 keeps its media in a temporary file and writes moov first, and AVI needs a seekable stream (NotSupportedException otherwise). leaveOpen keeps both streams open. The jobs are synchronous and block a thread while the network is slow; run them with Task.Run to keep a UI responsive.

// Any Stream works, seekable or not: a cloud blob's read and upload streams, an HTTP response body, a pipe.
// Files stand in for them here.
using Stream source = File.OpenRead("movie.webm");
using Stream upload = File.Create("movie.mkv");
long packets = MediaFile.Remux(source, upload, "mkv", leaveOpen: true, cancellationToken: cts.Token);

Deinterlacing

Broadcast and camcorder video is often interlaced: each frame holds two fields, the even lines and the odd lines, shown a field apart in time. Deinterlacer turns such frames into progressive ones: each frame it hands out keeps one field's lines and makes the other's. DeinterlaceMode.Adaptive, the default, keeps the other field's lines where the picture holds still, so fine detail stays sharp, and where it moves guesses each missing pixel along the edge through it, held within what the fields before and after allow, as the published yadif method does. DeinterlaceMode.Bob guesses from the kept field alone: it never combs, but halves the vertical detail of still parts.

DeinterlaceRate.Frame, the default, gives one frame per frame from the first field in time, so 50i becomes 25p; DeinterlaceRate.Field gives one per field, at twice the rate and half the duration each, so motion stays as smooth as it was shot. DeinterlaceFrames.Flagged, the default, deinterlaces the frames that say they are interlaced (H.264 flags each frame; for other codecs the track's field order counts) and passes the rest unchanged; All deinterlaces every frame. The field order comes from the frame, then the track, then top field first; DeinterlacerOptions.FieldOrder overrides it.

The deinterlacer is an IVideoFilter: frames go in with Send and come out with TryReceive, like a decoder, and it holds one frame back to look at the next, so call Flush at the end. It works on the decoder's planes: 8 to 16 bits, any chroma format, with alpha, each plane on its own lines, so a 4:2:0 field keeps its own chroma rows. On one thread it deinterlaces a 1080i frame in about 3.3 ms, and it allocates no pixel memory per frame once running.

// Deinterlace a file: interlaced frames come out progressive, one per frame in, and progressive frames pass unchanged.
// The track gives the field order for frames that do not carry their own.
using VideoReader reader = VideoReader.Open("movie.webm");
using Deinterlacer deinterlacer = new(reader.Track, new DeinterlacerOptions
{
    Mode = DeinterlaceMode.Adaptive,      // or Bob: from each field alone
    Rate = DeinterlaceRate.Frame,         // or Field: one frame per field, 50i becomes 50p
    Frames = DeinterlaceFrames.All,       // also frames the decoder does not flag as interlaced
});
VideoWriterOptions options = new() { Codec = VideoCodecs.Ffv1, FrameRate = reader.Track.FrameRate ?? new Rational(25, 1) };
using (VideoWriter writer = VideoWriter.Create("progressive.mkv", options))
{
    foreach (YuvVideoFrame frame in reader.ReadFrames())
    {
        using (frame)
        {
            deinterlacer.Send(frame);
        }

        while (deinterlacer.TryReceive(out YuvVideoFrame? output))
        {
            using (output)
            {
                writer.WriteFrame(output);
                written++;
            }
        }
    }

    // The deinterlacer holds one frame back to look at the next; Flush hands it out.
    deinterlacer.Flush();
    while (deinterlacer.TryReceive(out YuvVideoFrame? output))
    {
        using (output)
        {
            writer.WriteFrame(output);
            written++;
        }
    }
}

An edit deinterlaces a clip with Clip.Deinterlace, which is off by default; see Video editing.

Codecs by hand

VideoCodecs finds the codec for a track and creates an IVideoDecoder: send packets, receive frames. VideoCodecs.CreateDecoder<TPixel> gives an IVideoDecoder<TPixel> that hands out RGB frames.

// A codec by hand: packets in, frames out. VideoCodecs finds the codec for a track's CodecId (CodecIds.Vp9 here).
using IDemuxer demuxer = MediaFile.OpenDemuxer("movie.webm");
VideoTrackInfo track = demuxer.Info.VideoTrack!;
bool isVp9 = track.CodecId == CodecIds.Vp9;
using IVideoDecoder decoder = VideoCodecs.CreateDecoder(track, new VideoDecoderOptions { Errors = DecodeErrorMode.Throw });
int frames = 0;
while (demuxer.ReadPacket() is { } packet)
{
    using (packet)
    {
        decoder.Send(packet);
    }

    while (decoder.TryReceive(out YuvVideoFrame? frame))
    {
        using (frame)
        {
            frames++;
        }
    }
}

decoder.Flush();

Each codec is a VideoCodec: Av1Codec, Vp9Codec, Vp8Codec, Ffv1Codec, MotionJpegCodec and RawVideoCodec.

// The built-in codecs, for a MediaContext of your own or to create decoders directly.
VideoCodec vp9 = Vp9Codec.Instance, vp8 = Vp8Codec.Instance, mjpeg = MotionJpegCodec.Instance, raw = RawVideoCodec.Instance;
using IVideoDecoder decoder = vp9.CreateDecoder(track, VideoDecoderOptions.Default, MediaContext.Default);

// Decoders that hand out RGB frames instead of planes.
using IVideoDecoder<Rgba8> rgb = VideoCodecs.CreateDecoder<Rgba8>(track);

An IVideoEncoder takes frames and gives packets, with VideoEncoderOptions for the size, frame rate and quality.

// An encoder by hand: frames in, packets out, for a muxer of your own.
VideoEncoderOptions options = new() { Width = duck.Width, Height = duck.Height, FrameRate = new Rational(30, 1), Quality = 90 };
using IVideoEncoder encoder = MotionJpegCodec.Instance.CreateEncoder(options, MediaContext.Default);
using (VideoFrame<Rgb8> frame = new(Raster.Load<Rgb8>("duck.png"), MediaTime.Zero))
{
    encoder.Send(frame);
}

encoder.Flush();
List<EncodedPacket> packets = [];
while (encoder.TryReceive(out EncodedPacket? packet))
{
    packets.Add(packet);
}

// Uncompressed frames go the same way, losslessly.
using IVideoEncoder rawEncoder = RawVideoCodec.Instance.CreateEncoder(options with { ChromaFormat = ChromaFormat.Yuv444 }, MediaContext.Default);

CodecHeaders.Describe lists a packet's header fields as CodecHeaderFields under the codec's own names, for VP8, VP9, AV1, FFV1 and Motion JPEG.

// A packet's header fields under the codec's own names, as a hex dump with meaning.
using IDemuxer demuxer = MediaFile.OpenDemuxer("movie.webm");
TrackInfo track = demuxer.Tracks[0];
using EncodedPacket packet = demuxer.ReadPacket()!;
if (CodecHeaders.CanDescribe(track))
{
    foreach (CodecHeaderField field in CodecHeaders.Describe(track, packet))
    {
        Console.WriteLine($"{field.Name} = {field.Value}");
    }
}

AV1

Av1Codec decodes AV1 from WebM, Matroska, MP4 and IVF with Egami's AV1 decoder: every profile, 8, 10 and 12 bit, 4:0:0, 4:2:0, 4:2:2 and 4:4:4, film grain, scaled references and scalable streams. Every frame equals libaom's. A packet is a temporal unit; hidden frames give no frame, and a frame shown later with show_existing_frame comes out then. Frames come out at their own size, 8-bit planes for 8-bit streams and 16-bit planes above, 4:0:0 for monochrome.

  • The color comes from the sequence header; the container's fills only what the header leaves unspecified. HDR metadata OBUs give the frames' Hdr and Hdr10Plus (see HDR).
  • VideoDecoderOptions.ApplyFilmGrain = false gives the frames as coded, without the film grain the stream asks for.
  • A keyframe is a temporal unit that starts with a shown key frame. A forward key frame, hidden and shown later, is not one, as FFmpeg flags them too. When a container's keyframe flag disagrees, the stream wins and Av1.KeyframeFlagMismatch is reported.
  • A track's av1C (its CodecPrivate) gives the sequence header when the first unit has none. The IVF reader builds av1C from the first sequence header, and so does the Matroska writer for a track that has none, so IVF files remux to WebM, Matroska and MP4.
  • Seeking decodes from the shown key frame at or before the time, so exact seeks give the frame decoding from the start gives.
  • Broken data follows VideoReaderOptions.Errors: Skip (the default) reports Av1.CorruptFrame, or Av1.MissingReference for a unit that needs frames from before the first key frame, and resumes at the next shown key frame; Conceal repeats the last good frame in between.
// AV1 from WebM, Matroska, MP4 or IVF reads like any other video: 10 and 12-bit streams give 16-bit planes.
using (VideoReader reader = VideoReader.Open("av1.webm"))
{
    foreach (YuvVideoFrame frame in reader.ReadFrames())
    {
        using (frame)
        {
            Console.WriteLine($"{frame.Timestamp}: {frame.Width}x{frame.Height}, {frame.Raster.BitDepth} bit {frame.Raster.ChromaFormat}, {frame.Color}");
        }
    }
}

// The frames as coded, without the film grain the stream asks for; broken units are skipped to the next key frame and
// reported as Av1.CorruptFrame or Av1.MissingReference, or repeat the last good frame with Conceal.
VideoReaderOptions options = new()
{
    Decoder = new VideoDecoderOptions { ApplyFilmGrain = false },
    Errors = DecodeErrorMode.Conceal,
};
using VideoReader plain = VideoReader.Open("av1.webm", options);
using YuvVideoFrame first = plain.ReadFrame()!;

Raw AV1 streams are containers of their own: Av1ObuFormat reads and writes low-overhead OBU files (.obu, as aomenc --obu writes and aomdec and dav1d read), and Av1AnnexBFormat the length-delimited files of the specification's Annex B (as the conformance streams and aomenc --annexb=1 write). Both are detected from their first bytes; neither has timestamps, so frames are timed at the format's FrameRate, 30 a second unless you register one with another rate. Reading a file and writing it back gives the same bytes, and IVF, WebM, Matroska and MP4 tracks write to them.

// Raw AV1 streams have no timestamps: frames are timed at the format's frame rate, 30 a second unless you register another.
MediaContext context = MediaContext.Default.WithContainer(new Av1ObuFormat { FrameRate = new Rational(24, 1) });

// An IVF clip to a low-overhead .obu file (what aomdec and dav1d read) and to Annex B, then read back.
MediaFile.Remux("clip-av1.ivf", "clip.obu");
using (IDemuxer demuxer = MediaFile.OpenDemuxer("clip-av1.ivf"))
using (IMuxer annexB = Av1AnnexBFormat.Instance.CreateMuxer(File.Create("clip-annexb.obu"), demuxer.Tracks, MediaContext.Default, leaveOpen: false))
{
    while (demuxer.ReadPacket() is { } packet)
    {
        using (packet)
        {
            annexB.WritePacket(packet);
        }
    }
}

using VideoReader reader = VideoReader.Open("clip.obu", context: context);
Console.WriteLine($"{MediaFile.Probe("clip-annexb.obu").FormatName}; {reader.Track.FrameRate} fps");

Av1Packets splits a temporal unit into its frames and merges them back, and builds the av1C record from a sequence header.

// An AV1 packet is a temporal unit: split it into its frames and join them back; build the av1C an MP4 or Matroska
// track needs from its sequence header.
using IDemuxer demuxer = MediaFile.OpenDemuxer("clip-av1.ivf");
using EncodedPacket packet = demuxer.ReadPacket()!;
IReadOnlyList<ReadOnlyMemory<byte>> frames = Av1Packets.SplitFrames(packet.Data);
byte[] unit = Av1Packets.MergeFrames(frames);
byte[]? av1C = Av1Packets.ExtractConfig(packet.Data.Span);
Console.WriteLine($"{frames.Count} frames, av1C of {av1C?.Length} bytes");

CodecHeaders.Describe lists an AV1 packet's OBUs, its sequence header and metadata fields, and the start of each frame header (up to error_resilient_mode). The rest of a frame header depends on the frames before it, so a CodecHeaderReader reads a track's packets in order, decoding them, and lists each frame header whole.

// An AV1 frame header depends on the frames before it: a CodecHeaderReader reads the packets in order and lists each whole.
using IDemuxer demuxer = MediaFile.OpenDemuxer("av1.webm");
using CodecHeaderReader headers = new(demuxer.Tracks[0]);
while (demuxer.ReadPacket() is { } packet)
{
    using (packet)
    {
        IReadOnlyList<CodecHeaderField> fields = headers.Describe(packet);
        Console.WriteLine(string.Join(", ", fields.Where(f => f.Name is "frame_type" or "order_hint" or "base_q_idx").Select(f => $"{f.Name} = {f.Value}")));
    }
}

FFV1

Ffv1Codec decodes FFV1, the lossless codec archives use (RFC 9043), from Matroska (V_FFV1) and AVI (FourCC FFV1): versions 0, 1 and 3, Golomb-Rice and the range coder, YCbCr 4:2:0, 4:2:2, 4:4:4, 4:1:1, 4:1:0 and gray, RGB, each with or without alpha, at 8 to 16 bits. Every frame equals FFmpeg's sample for sample.

  • Frames keep their planes: 8-bit planes, or 16-bit planes holding 10, 12 or 16 bits. Depths 9, 11, 13, 14 and 15 sit in the next of those with their samples shifted up, so nothing is lost; the track's BitDepth gives the source depth. RGB comes out as G, B and R planes tagged YuvMatrix.Identity, which ToRaster turns back into RGB exactly.
  • A version 3 track's configuration record (its CodecPrivate) gives the depth and subsampling before any frame is decoded.
  • Every slice CRC is checked. Broken data follows VideoReaderOptions.Errors: a damaged slice is reported as Ffv1.SliceCrc (or Ffv1.CorruptFrame when it cannot be read), a frame with no keyframe before it as Ffv1.MissingReference and a damaged configuration record as Ffv1.ConfigCrc. Skip drops the frame, Conceal shows the last good frame where the slice was.
  • CodecHeaders.Describe lists the parameters, the keyframe flag and each slice's header and footer.
// FFV1 archives from Matroska or AVI read like any other video. A 10-bit master gives 16-bit planes holding 10 bits;
// the track says the depth and subsampling before any frame is decoded.
using VideoReader reader = VideoReader.Open("master.mkv", new VideoReaderOptions { Errors = DecodeErrorMode.Conceal }, context);
Console.WriteLine($"{reader.Track.CodecId}: {reader.Track.BitDepth} bit {reader.Track.ChromaFormat}");
foreach (YuvVideoFrame frame in reader.ReadFrames())
{
    using (frame)
    {
        Console.WriteLine($"{frame.Timestamp}: {frame.Raster.BitDepth} bit {frame.Raster.ChromaFormat}");
    }
}

// Every slice's CRC is checked: a damaged one is reported as Ffv1.SliceCrc and, with Conceal, shows the last good frame.
Console.WriteLine($"{reported.Count(d => d.Id == "Ffv1.SliceCrc")} damaged slices");

Slices decode in parallel up to EgamiContext.MaxParallelism, with the same frame at any thread count.

Ffv1Codec.Verify checks a file's fixity without decoding pictures: the configuration record's CRC and every slice's, far faster than a decode. The Ffv1VerifyResult says how many frames and slices it checked and lists each Ffv1VerifyError; a version 0 or 1 file, or one written without slice CRCs, has HasSliceCrcs false.

// Check an archive's fixity without decoding pictures: the configuration record's CRC and every slice's.
Ffv1VerifyResult result = Ffv1Codec.Verify("master.mkv");
Console.WriteLine($"{result.Frames} frames, {result.Slices} slices checked: {(result.IsIntact ? "intact" : "damaged")}");
foreach (Ffv1VerifyError error in result.Errors)
{
    Console.WriteLine($"frame {error.Frame} at {error.Timestamp}, slice {error.Slice}: {error.Problem}");
}

Writing FFV1 is in Encoding video; Lossless archiving with FFV1 puts writing, fixity checks and restores together.

HDR

A frame's Color and Hdr carry its color code points and static HDR metadata. YuvVideoFrame.ToRaster with a target ColorProfile converts through Egami's color pipeline and tone maps PQ and HLG to SDR. VP9 frames in WebM carry HDR10+ dynamic metadata as Hdr10PlusMetadata, with its Hdr10PlusPercentile distribution and Hdr10PlusToneCurve. AV1 frames take Hdr from the stream's content light level and mastering display metadata OBUs (or the track's av1C), and Hdr10Plus from an ITU-T T.35 metadata OBU.

// An HDR10 frame tone mapped to SDR through Egami's color pipeline, from its mastering display and light levels.
using (VideoReader reader = VideoReader.Open("hdr10.mkv"))
using (YuvVideoFrame frame = reader.ReadFrame()!)
using (Raster<Rgba8> sdr = frame.ToRaster<Rgba8>(ColorProfile.Srgb))
{
    sdr.Save("sdr.png");
}

// HDR10+ dynamic metadata, frame by frame.
using VideoReader plus = VideoReader.Open("hdr10plus.webm");
foreach (YuvVideoFrame frame in plus.ReadFrames())
{
    using (frame)
    {
        if (frame.Hdr10Plus is { } meta)
        {
            double maxScl = meta.MaxScl.Max();                      // cd/m²
            Hdr10PlusPercentile median = meta.DistributionMaxRgb.First(p => p.Percentage == 50);
            Hdr10PlusToneCurve? curve = meta.ToneCurve;
            Console.WriteLine($"MaxSCL {maxScl}, median {median.Value} cd/m², knee {curve?.KneeX}");
        }
    }
}

Measuring quality

VideoQuality sums PSNR and SSIM over frames, plane by plane, as PlaneScores, and hashes frames the way libvpx's tests do.

// PSNR and SSIM of a decode against a reference, plane by plane, and the MD5 of a frame as libvpx's tests take it.
VideoQuality quality = new();
using (VideoReader reference = VideoReader.Open("movie.webm"))
using (VideoReader distorted = VideoReader.Open("movie.webm"))
{
    foreach ((YuvVideoFrame a, YuvVideoFrame b) in reference.ReadFrames().Zip(distorted.ReadFrames()))
    {
        using (a)
        using (b)
        {
            quality.Add(a.Raster, b.Raster);
            string md5 = VideoQuality.Md5(a.Raster);
        }
    }
}

PlaneScores psnr = quality.Psnr, ssim = quality.Ssim;
Console.WriteLine($"{quality.Frames} frames: PSNR Y {psnr.Y:0.00} dB, SSIM {ssim.Average:0.0000}");

Scenes and analysis

MediaAnalysis.Run reads a file once and hands every frame to its detectors: the SceneDetector by default, and any IVideoDetector or IAudioDetector you add. Each Scene has its first and last frame, its start and end time and how it starts (SceneStart): at the first frame, at a cut, or inside a fade through black. Cuts lists each SceneCut and Fades each fade out and fade in (FadeKind).

// One pass over the file: the scene detector runs by default, and any detectors of your own run with it.
MediaAnalysisResult result = MediaAnalysis.Run("shots.mkv");
foreach (Scene scene in result.Scenes)
{
    Console.WriteLine($"Scene {scene.Number}: {scene.Start.ToTimeSpan()} to {scene.End.ToTimeSpan()}, starts with a {scene.StartKind}");
}

SceneOptions picks the method and its threshold. SceneMethod.Adaptive weighs each frame's change against the frames around it, so a fast pan is not taken for a cut; Content compares with the frames just before, as ffmpeg's scene score does; Histogram compares luma histograms, which ignores motion inside a steady picture. A cut closer than MinSceneLength to the last is dropped, and with FlashMode.Merge a flash that comes back to the picture before it is no scene. A frame counts as black for fades when its mean luma is at or under FadeLevel (2 % of the range, so dark scenes keep their cuts). KeepSeries keeps every frame's score.

// Content compares each frame with the one before, as ffmpeg's scene score does; Adaptive (the default) also weighs the
// frames around it, so fast motion is not taken for a cut. Fades through black end a scene in the dark.
SceneOptions options = new()
{
    Method = SceneMethod.Content,
    Threshold = 0.3,
    MinSceneLength = MediaTime.FromSeconds(0.5),
    DetectFades = true,
    KeepSeries = true,
};
MediaAnalysisResult result = MediaAnalysis.Run("shots.mkv", new MediaAnalysisOptions { Scenes = options });
SceneScore sharpest = result.SceneScores.MaxBy(s => s.Score);
Console.WriteLine($"{result.Cuts.Count} cuts; the sharpest at {sharpest.Time.ToTimeSpan()}");

A detector of your own takes frames in time order, and the SceneDetector runs on its own too:

// A detector takes frames in time order; this one finds the darkest frame. The scene detector can run on its own too.
DarkestFrame darkest = new();
SceneDetector scenes = new(new SceneOptions { Method = SceneMethod.Histogram });
using (VideoReader reader = VideoReader.Open("shots.mkv"))
{
    foreach (YuvVideoFrame frame in reader.ReadFrames())
    {
        using (frame)
        {
            darkest.Add(frame);
            if (scenes.Add(frame) is { } cut)
            {
                Console.WriteLine($"Cut at frame {cut.Frame}");
            }
        }
    }

    scenes.Complete(reader.Info.Duration ?? MediaTime.Zero);
}

Console.WriteLine($"Darkest frame at {darkest.Time.ToTimeSpan()}, {scenes.Scenes.Count} scenes");
// Audio detectors get the audio in blocks, interleaved with the video by time, in the same pass.
Peak peak = new();
MediaAnalysis.Run("shots.mkv", new MediaAnalysisOptions { AudioDetectors = [peak] });
Console.WriteLine($"Peak {20 * Math.Log10(peak.Value):0.0} dBFS");

SceneFrames.Read seeks to the first, middle or last frame of each scene, for thumbnails or a shot list:

// A frame from each scene, read by seeking: the first, the middle or the last.
MediaAnalysisResult result = MediaAnalysis.Run("shots.mkv");
using VideoReader reader = VideoReader.Open("shots.mkv");
foreach (SceneFrame scene in SceneFrames.Read(reader, result.Scenes, ScenePosition.Middle))
{
    using (scene.Frame)
    using (Raster<Rgba8> picture = scene.Frame.ToRaster<Rgba8>())
    {
        picture.Save($"scene-{scene.Scene.Number}.png");
    }
}

TestSources makes pictures and sounds to test with: SMPTE color bars at their code values in any bit depth, a moving pattern with a frame counter, solid colors, gradients, tones, white or pink noise (NoiseColor) and sweeps. TestVideoFormat and TestAudioFormat set the size, rate and sample layout; ColorBarsStyle picks HD or SD bars.

// Generated pictures and sounds for tests and demos: bars, a moving pattern, solid colors, gradients, tones and noise.
TestVideoFormat format = new() { Width = 320, Height = 180, FrameRate = new Rational(25, 1) };
MediaTime second = MediaTime.FromSeconds(1);
using VideoWriter writer = VideoWriter.Create(path, new VideoWriterOptions
{
    FrameRate = format.FrameRate,
    AudioCodec = Egami.Video.Audio.Codecs.FlacCodec.Instance,
    AudioSampleRate = 48000,
    AudioLayout = ChannelLayout.Stereo,
});
IEnumerable<YuvVideoFrame> shots = TestSources.ColorBars(format, second)
    .Concat(TestSources.Solid(format, second, new Rgba8(30, 40, 220, 255)))
    .Concat(TestSources.Pattern(format, second));
int index = 0;
foreach (YuvVideoFrame frame in shots)
{
    using (frame)
    {
        writer.WriteFrame(frame.Raster, new MediaTime(index++, format.FrameRate.Reciprocal()));
    }
}

foreach (AudioFrame block in TestSources.Sine(null, MediaTime.FromSeconds(3), frequency: 1000, level: -18))
{
    using (block)
    {
        writer.WriteAudio(block);
    }
}

More detectors run in the same pass, each keeping ffmpeg's documented measure and defaults with thresholds as fractions of the stream's range: BlackDetector (blackdetect) and FreezeDetector (freezedetect) return DetectedRanges, CropDetector (cropdetect) a CropReport with the rectangle that keeps every frame's picture, InterlaceDetector (idet) an InterlaceReport with field order counts, repeated fields and an InterlaceVerdict that spots 3:2 telecine, and DuplicateDetector (mpdecimate's test) runs of repeated frames. Each takes its own options record (BlackOptions, FreezeOptions, CropOptions with the runs of frames as CropRuns, InterlaceOptions, DuplicateOptions), and the black detector keeps every frame's ratio as a FrameValue series on request.

// ffmpeg's detection filters as detectors, with their documented defaults: black and frozen stretches, black borders,
// field order and repeated frames, all from one decode.
BlackDetector black = new(new BlackOptions { MinDuration = MediaTime.FromSeconds(0.5) });
using FreezeDetector freeze = new(new FreezeOptions { MinDuration = MediaTime.FromSeconds(0.5) });
CropDetector crop = new();
InterlaceDetector fields = new();
DuplicateDetector duplicates = new();
MediaAnalysis.Run("shots.mkv", new MediaAnalysisOptions { VideoDetectors = [black, freeze, crop, fields, duplicates] });

foreach (DetectedRange range in freeze.Ranges)
{
    Console.WriteLine($"Frozen from {range.Start.ToTimeSpan()} to {range.End.ToTimeSpan()}");
}

Console.WriteLine($"{black.Ranges.Count} black stretches, crop to {crop.Report().Crop}, {fields.Report().Verdict}");
Console.WriteLine($"{duplicates.DuplicateCount} repeated frames");

SignalMeter reports what signalstats and colordetect do: the lowest, 10 %, mean, 90 % and highest code values of each plane as PlaneLevels, saturation, the share of pixels outside broadcast range, the SignalRange the samples really use and the AlphaUse of an alpha plane. With Blur and Blockiness on it also scores edge width and coding blocks, and KeepSeries keeps a SignalFrame per frame.

// Levels, saturation and broadcast range as signalstats gives them, the range really used, and optional blur and
// blockiness scores.
SignalMeter meter = new(new SignalOptions { Blur = true, Blockiness = true });
MediaAnalysis.Run("shots.mkv", new MediaAnalysisOptions { Scenes = null, VideoDetectors = [meter] });
SignalReport report = meter.Report();
Console.WriteLine($"Y {report.Y.Min}..{report.Y.Max} (mean {report.Y.Mean:0.0}), {report.OutOfRange:P1} out of range, {report.Range} range");
Console.WriteLine($"Edges {report.Blur:0.00} px wide, blockiness {report.Blockiness:0.00}");

On the audio side, SilenceDetector finds silent stretches as silencedetect does, over all channels or each one, with its SilenceOptions; AudioLevelMeter returns an AudioLevelReport with ChannelLevels for each channel and overall: peak, RMS, DC offset, crest factor, clipped samples, noise floor, zero crossings and a 1 dB histogram, as volumedetect and astats report them.

// Silent stretches as silencedetect finds them (under -60 dBFS for 2 s by default), and the sample levels volumedetect
// and astats report, per channel and over all channels.
SilenceDetector silence = new(new SilenceOptions { Noise = -50, MinDuration = MediaTime.FromSeconds(0.5) });
AudioLevelMeter levels = new();
MediaAnalysis.Run("shots.mkv", new MediaAnalysisOptions { Scenes = null, AudioDetectors = [silence, levels] });

Console.WriteLine($"{silence.Ranges.Count} silent stretches");
AudioLevelReport report = levels.Report();
Console.WriteLine($"Peak {report.Overall.Peak:0.0} dBFS, RMS {report.Overall.Rms:0.0} dBFS, {report.Overall.Clipped} clipped");

LoudnessMeter measures loudness as ITU-R BS.1770 and EBU R 128 define it, at any rate from 8 to 384 kHz: momentary and short-term loudness while audio comes in, and a LoudnessReport with the gated integrated loudness, the loudness range (EBU Tech 3342) and the true peak. LoudnessOptions turns the true peak off or keeps a LoudnessValue every 100 ms.

// EBU R 128 loudness: integrated, range and true peak at the end, momentary and short-term while the audio plays.
LoudnessMeter meter = new(new LoudnessOptions { KeepSeries = true });
MediaAnalysis.Run("shots.mkv", new MediaAnalysisOptions { Scenes = null, AudioDetectors = [meter] });
LoudnessReport loudness = meter.Report();
Console.WriteLine($"I {loudness.Integrated:0.0} LUFS, LRA {loudness.Range:0.0} LU, true peak {loudness.TruePeak:0.0} dBTP");
foreach (LoudnessValue value in meter.ShortTermSeries)
{
    Console.WriteLine($"{value.Time.ToTimeSpan()}: {value.Loudness:0.0} LUFS");
}

AudioPictures draws audio as a Raster<Rgba8>, reading it once with constant memory however long it runs. Waveform shows each column's min-to-max outline with the RMS level shaded inside, linear or in dB (WaveformScale), with the channels together or stacked (AudioPictureChannels); WaveformOptions sets the size and colors. Spectrogram shows frequency over time with a Hann-windowed FFT per column, on a linear or logarithmic FrequencyScale, colored by level from Floor to Ceiling with magma or grayscale (SpectrogramColors); SpectrogramOptions sets the window length too. The pictures carry no text: add axes and labels with Egami.Drawing.

// A waveform with each channel in its own strip, and a spectrogram on a logarithmic frequency axis in magma colors.
using Raster<Rgba8> waveform = AudioPictures.Waveform("shots.mkv", new WaveformOptions
{
    Width = 1200,
    Height = 240,
    Channels = AudioPictureChannels.Stacked,
});
waveform.Save("waveform.png");

using Raster<Rgba8> spectrogram = AudioPictures.Spectrogram("shots.mkv", new SpectrogramOptions
{
    Width = 1200,
    Height = 400,
    Scale = FrequencyScale.Logarithmic,
    Floor = -100,
});
spectrogram.Save("spectrogram.png");

MediaAnalysis.Run, AudioPictures.Waveform and AudioPictures.Spectrogram also take a Stream, with the same options, for an upload or a download. A seekable stream is read from its position as a file would be. A stream that cannot seek is read once: the analysis shares its video and audio packets out as they come and decodes up to Start instead of seeking, and the pictures draw in the same pass when the container gives the duration, or else copy the stream to a temporary file on the way, deleted after. The caller owns the stream; it is never disposed.

// From a stream, such as an upload or a download, with the same options as from a file. A stream that cannot seek
// is read once; the caller keeps the stream and disposes it.
using Raster<Rgba8> picture = AudioPictures.Waveform(download, new WaveformOptions { Width = 800, Height = 120 });
picture.Save("upload-waveform.png");

using FileStream file = File.OpenRead("shots.mkv");
MediaAnalysisResult result = MediaAnalysis.Run(file, new MediaAnalysisOptions { AudioDetectors = [new SilenceDetector()] });
Console.WriteLine($"{result.Scenes.Count} scenes in {result.VideoFrames} frames");

Limits and warnings

A MediaContext holds the containers and codecs, the Egami context (memory, threads, safety limits), the MediaLimits for files from outside (packet, header and index sizes, tracks, nesting) and a handler for MediaWarnings: what a reader recovered from. VP9 decodes tile columns and loop filter rows on up to MaxParallelism threads.

// Limits for files from outside, and a handler for what a reader recovers from (a broken index, a cut-off cluster).
List<MediaWarning> warnings = [];
MediaContext context = MediaContext.Default with
{
    Limits = MediaLimits.Default with { MaxPacketBytes = 8 << 20, MaxTracks = 8 },
    Warnings = warnings.Add,
    Egami = EgamiContext.Default with { MaxParallelism = 2 },
};
using VideoReader reader = VideoReader.Open("movie.webm", null, context);
foreach (MediaWarning warning in warnings)
{
    Console.WriteLine($"{warning.Code}: {warning.Message}");
}

MP4 and QuickTime

Mp4Format reads .mp4, .m4v, .m4a and .mov files, flat or fragmented (CMAF and DASH segments, phone recordings), and writes ISO MP4. Every track is read: AV1, VP9, VP8, Motion JPEG, Opus, FLAC, PCM and mov_text with their settings, and H.264, HEVC, AAC or any other codec passed through, so a remux keeps them byte for byte. Opening a long file reads only its index.

  • Edit lists are applied, as FFmpeg shows them: a delayed track starts later, and packets before the start are marked EncodedPacket.IsDiscardable (the reader decodes them and drops their frames). new Mp4Format { IgnoreEditLists = true } gives the media's own times.
  • The display matrix is VideoTrackInfo.Orientation. Frames come out as coded; VideoReaderOptions.AutoOrient turns them.
  • A timecode track gives MediaInfo.Timecode. Tags, cover art (as an attachment named cover.jpg or cover.png) and chapters (Nero chpl or a QuickTime chapter track) are read, and written back.
  • MovTextSample reads and writes mov_text subtitles with their style runs (MovTextStyle: the characters each covers, MovTextFace bold, italic or underline, size and color).
  • Writing puts moov before the media, so the file plays while it downloads. On a stream that can seek and read the media is moved forward at the end; on any other stream the file is written fragmented (see Streaming MP4, which also covers HLS and DASH segments). new Mp4Format { MoovAtEnd = true } writes moov last, straight to a seekable stream. A QuickTime file is written as MP4.
// MP4 and QuickTime files read like any other: rotation from the display matrix, the start timecode, edit lists applied.
MediaInfo phone = MediaFile.Probe("phone.mov");
Console.WriteLine($"{phone.FormatName}: turn {phone.VideoTrack!.Orientation} to show upright");
Console.WriteLine($"starts at timecode {MediaFile.Probe("camera.mov").Timecode}");

// Frames come out as coded; AutoOrient turns each one upright.
using (VideoReader reader = VideoReader.Open("phone.mov", new VideoReaderOptions { AutoOrient = true }))
{
    using VideoFrame<Rgb8> upright = reader.ReadFrames<Rgb8>().First();
    Console.WriteLine($"{reader.Track.Width}x{reader.Track.Height} coded, {upright.Raster.Width}x{upright.Raster.Height} shown");
}

// Writing MP4: moov before the media (the file plays while it downloads), tags, chapters and cover art kept.
MediaFile.Remux("movie.webm", "movie.mp4");
MediaFile.Remux("metadata.mkv", "metadata.mp4");

// mov_text subtitles: each packet's text and style runs.
using IDemuxer demuxer = MediaFile.OpenDemuxer("subtitled.mp4");
TrackInfo subtitles = demuxer.Tracks.First(t => t.CodecId == CodecIds.MovText);
while (demuxer.ReadPacket() is { } packet)
{
    using (packet)
    {
        if (packet.TrackNumber == subtitles.Number)
        {
            MovTextSample text = MovTextSample.Parse(packet.Data.Span);
            Console.WriteLine($"{packet.Timestamp.ToTimeSpan()}: {text.Text} ({text.Styles.Count} styles)");
        }
    }
}

Broken files are read up to the damage, with a warning through MediaContext.Warnings: mp4-truncated (the file was cut off), mp4-bad-track, mp4-bad-box and mp4-bad-fragment (broken boxes, skipped), mp4-sample-table (tables that disagree), mp4-edit-list (an edit list with several media edits or another rate, not applied), mp4-sample-descriptions (a track that changes its sample description), mp4-fragment-track and mp4-track-id. Writers report mp4-timestamps (a packet decoding before the one before it), metadata-dropped, and for fragmented files mp4-fragmented and mp4-chapter-track. A file whose moov follows its media needs a stream that can seek.

Containers

Each container is a ContainerFormat: MatroskaFormat (WebM and Matroska), Mp4Format (MP4 and QuickTime), AviFormat, IvfFormat, Y4mFormat, Av1ObuFormat and Av1AnnexBFormat for raw AV1, and for audio WavFormat and FlacFormat. RawVideo reads and writes headerless frames in a layout you give.

// The containers, by name or extension, and headerless raw frames in a layout you give.
ContainerFormat? webm = MediaContext.Default.FindContainer("webm");
bool known = webm is MatroskaFormat && MediaContext.Default.FindContainer("avi") is AviFormat
    && MediaContext.Default.FindContainer("ivf") is IvfFormat && MediaContext.Default.FindContainer("y4m") is Y4mFormat;

VideoTrackInfo track = new()
{
    Number = 1,
    Kind = TrackKind.Video,
    CodecId = CodecIds.Uncompressed,
    FourCC = "I420",
    TimeBase = new Rational(1, 25),
    FrameRate = new Rational(25, 1),
    Width = 64,
    Height = 48,
};
using MemoryStream frames = new(new byte[64 * 48 * 3 / 2 * 2]);
using IDemuxer raw = RawVideo.OpenDemuxer(frames, track);