HEVC / H.265 Access Unit Decoder

This article explains how you can use {transcoder-push-net} to decode HEVC / H.265 access units to raw YUV video frames.

The code snippets in this article are from the dec_hevc_au .NET sample.

Source Video

As video input we use the foreman_qcif.h265.au directory from the AVBlocks Assets archive. After downloading and unzipping you will find foreman_qcif.h265.au in the vid subdirectory. The directory contains HEVC access units named au_####.h265.

Code

This code reads a sequence of HEVC access-unit files, pushes each access unit into a Transcoder, and writes decoded raw YUV video frames to a file.

Initialize AVBlocks

The first step in any AVBlocks application is to initialize the library. This must be done before using any other AVBlocks functionality. Call Library.Shutdown() before the program exits to clean up resources.

static int Main(string[] args)
{
    var opt = new Options();

    if (!opt.Prepare(args))
        return opt.Error ? (int)ExitCodes.OptionsError : (int)ExitCodes.Success;

    Library.Initialize();

    // Set license information. To run AVBlocks in demo mode, comment the next line out
    // Library.SetLicense("<license-string>");

    bool decodeResult = DecodeAUs(opt);

    Library.Shutdown();

    return decodeResult ? (int)ExitCodes.Success : (int)ExitCodes.DecodingError;
}

Create Output Socket

The output socket is configured for uncompressed video. The sample uses the color format from the command line when provided. The output file path is specified on the command line.

static MediaSocket CreateOutputSocket(Options opt)
{
    MediaSocket socket = new MediaSocket();
    socket.File = opt.OutputFile;
    socket.StreamType = StreamType.UncompressedVideo;

    MediaPin pin = new MediaPin();
    socket.Pins.Add(pin);

    VideoStreamInfo vsi = new VideoStreamInfo();
    pin.StreamInfo = vsi;

    vsi.StreamType = StreamType.UncompressedVideo;
    vsi.ColorFormat = opt.ColorId;
    vsi.ScanType = ScanType.Progressive;

    return socket;
}

Configure Transcoder

The sample uses MediaInfo on the first access-unit file to detect the input stream properties. It then creates an input socket from that media information, clears the file and stream from the socket, creates the output socket, and opens the transcoder.

static bool ConfigureTranscoder(Transcoder transcoder, string auFile, Options opt)
{
    using (var mediaInfo = new MediaInfo())
    {
        mediaInfo.Inputs[0].File = auFile;

        if (!mediaInfo.Open())
        {
            PrintError("MediaInfo open", mediaInfo.Error);
            return false;
        }

        // create input socket from media info
        MediaSocket inSocket = MediaSocket.FromMediaInfo(mediaInfo);
        inSocket.File = null;
        inSocket.Stream = null;

        // create output socket
        MediaSocket outSocket = CreateOutputSocket(opt);

        transcoder.Inputs.Add(inSocket);
        transcoder.Outputs.Add(outSocket);
    }

    return true;
}

Push Access Units

The main transcode loop reads files named au_####.h265 from the input directory. Each file is loaded into a MediaBuffer, attached to a MediaSample, and pushed into the transcoder. After all access units are pushed, the sample flushes the transcoder to write delayed decoded frames.

static bool DecodeAUs(Options opt)
{
    // delete output file if exists
    DeleteFile(opt.OutputFile);

    // create output directory
    string outputDir = Path.GetDirectoryName(opt.OutputFile);
    if (!string.IsNullOrEmpty(outputDir))
        Directory.CreateDirectory(outputDir);

    string firstAuFile = BuildAuPath(opt, 0);
    if (!File.Exists(firstAuFile))
    {
        Console.WriteLine("First AU file not found: " + firstAuFile);
        return false;
    }

    using (var transcoder = new Transcoder())
    {
        transcoder.AllowDemoMode = true;

        // configure transcoder using first AU file
        if (!ConfigureTranscoder(transcoder, firstAuFile, opt))
            return false;

        if (!transcoder.Open())
        {
            PrintError("Transcoder open", transcoder.Error);
            return false;
        }

        // process all AU files
        for (int i = 0; ; i++)
        {
            string auFile = BuildAuPath(opt, i);
            if (!File.Exists(auFile))
                break;

            var sample = new MediaSample();
            sample.Buffer = new MediaBuffer(File.ReadAllBytes(auFile));

            if (!transcoder.Push(0, sample))
            {
                PrintError("Transcoder push", transcoder.Error);
                return false;
            }
        }

        if (!transcoder.Flush())
        {
            PrintError("Transcoder flush", transcoder.Error);
            return false;
        }

        transcoder.Close();
    }

    Console.WriteLine("Output file: " + opt.OutputFile);
    return true;
}

Complete Code

Here’s the complete working example that demonstrates HEVC / H.265 access-unit decoding using AVBlocks for .NET.

using System;
using System.IO;
using PrimoSoftware.AVBlocks;

namespace CliSample
{
    class Program
    {
        static int Main(string[] args)
        {
            var opt = new Options();

            if (!opt.Prepare(args))
                return opt.Error ? (int)ExitCodes.OptionsError : (int)ExitCodes.Success;

            Library.Initialize();

            // Set license information. To run AVBlocks in demo mode, comment the next line out
            // Library.SetLicense("<license-string>");

            bool decodeResult = DecodeAUs(opt);

            Library.Shutdown();

            return decodeResult ? (int)ExitCodes.Success : (int)ExitCodes.DecodingError;
        }

        static bool DecodeAUs(Options opt)
        {
            // delete output file if exists
            DeleteFile(opt.OutputFile);

            // create output directory
            string outputDir = Path.GetDirectoryName(opt.OutputFile);
            if (!string.IsNullOrEmpty(outputDir))
                Directory.CreateDirectory(outputDir);

            string firstAuFile = BuildAuPath(opt, 0);
            if (!File.Exists(firstAuFile))
            {
                Console.WriteLine("First AU file not found: " + firstAuFile);
                return false;
            }

            using (var transcoder = new Transcoder())
            {
                transcoder.AllowDemoMode = true;

                // configure transcoder using first AU file
                if (!ConfigureTranscoder(transcoder, firstAuFile, opt))
                    return false;

                if (!transcoder.Open())
                {
                    PrintError("Transcoder open", transcoder.Error);
                    return false;
                }

                // process all AU files
                for (int i = 0; ; i++)
                {
                    string auFile = BuildAuPath(opt, i);
                    if (!File.Exists(auFile))
                        break;

                    var sample = new MediaSample();
                    sample.Buffer = new MediaBuffer(File.ReadAllBytes(auFile));

                    if (!transcoder.Push(0, sample))
                    {
                        PrintError("Transcoder push", transcoder.Error);
                        return false;
                    }
                }

                if (!transcoder.Flush())
                {
                    PrintError("Transcoder flush", transcoder.Error);
                    return false;
                }

                transcoder.Close();
            }

            Console.WriteLine("Output file: " + opt.OutputFile);
            return true;
        }

        static bool ConfigureTranscoder(Transcoder transcoder, string auFile, Options opt)
        {
            using (var mediaInfo = new MediaInfo())
            {
                mediaInfo.Inputs[0].File = auFile;

                if (!mediaInfo.Open())
                {
                    PrintError("MediaInfo open", mediaInfo.Error);
                    return false;
                }

                // create input socket from media info
                MediaSocket inSocket = MediaSocket.FromMediaInfo(mediaInfo);
                inSocket.File = null;
                inSocket.Stream = null;

                // create output socket
                MediaSocket outSocket = CreateOutputSocket(opt);

                transcoder.Inputs.Add(inSocket);
                transcoder.Outputs.Add(outSocket);
            }

            return true;
        }

        static string BuildAuPath(Options opt, int index)
        {
            string pattern = "au_{0:0000}.h265";
            string path = Path.Combine(opt.InputDir, string.Format(pattern, index));
            return path;
        }

        static MediaSocket CreateOutputSocket(Options opt)
        {
            MediaSocket socket = new MediaSocket();
            socket.File = opt.OutputFile;
            socket.StreamType = StreamType.UncompressedVideo;

            MediaPin pin = new MediaPin();
            socket.Pins.Add(pin);

            VideoStreamInfo vsi = new VideoStreamInfo();
            pin.StreamInfo = vsi;

            vsi.StreamType = StreamType.UncompressedVideo;
            vsi.ColorFormat = opt.ColorId;
            vsi.ScanType = ScanType.Progressive;

            return socket;
        }

        static void DeleteFile(string filename)
        {
            try
            {
                if (File.Exists(filename))
                    File.Delete(filename);
            }
            catch { }
        }

        static void PrintError(string action, ErrorInfo e)
        {
            if (action != null)
            {
                Console.Write("{0}: ", action);
            }

            if (ErrorFacility.Success == e.Facility)
            {
                Console.WriteLine("Success");
                return;
            }
            else
            {
                Console.WriteLine("{0}, facility:{1} code:{2} hint:{3}", e.Message ?? "", e.Facility, e.Code, e.Hint ?? "");
            }
        }

        enum ExitCodes : int
        {
            Success = 0,
            OptionsError = 1,
            DecodingError = 2,
        }
    }
}

How to Run

See the build instructions for macOS and the dec_hevc_au .NET sample for details.

Command Line

dec_hevc_au --input <directory> --output <file.yuv> [--color <COLOR>]

Examples

List options:

./bin/net10.0/dec_hevc_au --help

List supported color formats:

./bin/net10.0/dec_hevc_au --colors

Decode the H.265 Access Units from ./assets/vid/foreman_qcif.h265.au/ into output file ./output/dec_hevc_au/foreman_qcif.yuv:

# Linux and macOS
mkdir -p ./output/dec_hevc_au

./bin/net10.0/dec_hevc_au \
    --input ./assets/vid/foreman_qcif.h265.au \
    --output ./output/dec_hevc_au/foreman_qcif.yuv \
    --color yuv420
# Windows
mkdir -Force -Path ./output/dec_hevc_au

./bin/net10.0/dec_hevc_au `
    --input ./assets/vid/foreman_qcif.h265.au `
    --output ./output/dec_hevc_au/foreman_qcif.yuv `
    --color yuv420