HEVC / H.265 Access Unit Decoder

This article explains how you can use Transcoder::push to decode HEVC / H.265 access units to raw YUV video frames.

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

Linux and Windows samples are also available:

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.

int main(int argc, char* argv[])
{
    Options opt;

    switch(prepareOptions(opt, argc, argv))
    {
        case Command: return 0;
        case Error:	return 1;
        case Parsed: break;
    }

    primo::avblocks::Library::initialize();

    // set your license string
    // primo::avblocks::Library::setLicense("PRIMO-LICENSE");

    bool transcodeResult = transcode(opt);

    primo::avblocks::Library::shutdown();

    return transcodeResult ? 0 : 1;
}

Configure Output Socket

The output socket is configured for uncompressed video. The sample uses the color format, frame size, and frame rate from the command line when provided. If the output file is not specified, it creates a default decoded_<width>x<height>.yuv file under output/dec_hevc_au.

MediaSocket* outputSocket(Options& opt, MediaSocket* inSocket)
{
    auto outSocket = primo::make_ref(Library::createMediaSocket());
    auto outPin = primo::make_ref(Library::createMediaPin());
    auto outVsi = primo::make_ref(Library::createVideoStreamInfo());

    outVsi->setColorFormat(opt.output_color.Id == ColorFormat::Unknown ? ColorFormat::YUV420 : opt.output_color.Id);

    if (opt.frame_size.height > 0)
        outVsi->setFrameHeight(opt.frame_size.height);

    if (opt.frame_size.width > 0)
        outVsi->setFrameWidth(opt.frame_size.width);

    if (opt.fps > 0)
        outVsi->setFrameRate(opt.fps);

    outVsi->setStreamType(StreamType::UncompressedVideo);
    outVsi->setScanType(ScanType::Progressive);

    outPin->setStreamInfo(outVsi.get());

    outSocket->pins()->add(outPin.get());

    if (opt.output_file.empty())
    {
        int yuv_width, yuv_height;

        if (opt.frame_size.height > 0 && opt.frame_size.width > 0)
        {
            yuv_width = opt.frame_size.width;
            yuv_height = opt.frame_size.height;
        }
        else
        {
            VideoStreamInfo* vsi = (VideoStreamInfo*)inSocket->pins()->at(0)->streamInfo();
            yuv_height = vsi->frameHeight();
            yuv_width = vsi->frameWidth();
        }

        // build output file name "path/decoded_wxh.yuv"
        fs::path output(getExeDir() + "/../../output/dec_hevc_au");
        fs::create_directories(output);

        ostringstream s;
        s << output.c_str() << "/decoded_" << yuv_width << "x" << yuv_height << ".yuv";
        opt.output_file = s.str();
    }

    outSocket->setFile(primo::ustring(opt.output_file));
    outSocket->setStreamType(StreamType::UncompressedVideo);

    return outSocket.release();
}

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.

bool setTranscoder(Transcoder* transcoder, string imgFile, Options& opt)
{
    auto info = primo::make_ref(Library::createMediaInfo());
    info->inputs()->at(0)->setFile(primo::ustring(imgFile));

    if (!info->open())
    {
        printError("MediaInfo open", info->error());
        return false;
    }

    // prepare input socket
    auto inSocket = primo::make_ref(Library::createMediaSocket(info.get()));
    inSocket->setFile(NULL);
    inSocket->setStream(NULL);

    // prepare output socket
    auto outSocket = primo::make_ref(outputSocket(opt, inSocket.get()));

    transcoder->inputs()->add(inSocket.get());
    transcoder->outputs()->add(outSocket.get());

    deleteFile(opt.output_file.c_str());

    if (!transcoder->open())
    {
        printError("Transcoder open", transcoder->error());
        return false;
    }

    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.

bool transcode(Options& opt)
{
    auto transcoder = primo::make_ref(Library::createTranscoder());
    transcoder->setAllowDemoMode(true);

    bool decoderInitialized = false;
    int32_t index = 0;

    for (int i = 0; ; i++)
    {
        char imgFile[PATH_MAX];
        string pattern = "au_%04d.h265";
        string imgPath = opt.input_dir + "/" + pattern;
        snprintf(imgFile, PATH_MAX, imgPath.c_str(), i);

        if (!decoderInitialized)
        {
            if (!setTranscoder(transcoder.get(), imgFile, opt))
                return false;

            decoderInitialized = true;
        }

        auto sample = primo::make_ref(Library::createMediaSample());

        vector<uint8_t> inputData = readFileBytes(imgFile);
        if (inputData.size() <= 0)
            break;

        auto buffer = primo::make_ref(Library::createMediaBuffer());
        buffer->attach(inputData.data(), inputData.size(), true);
        sample->setBuffer(buffer.get());

        if (!transcoder->push(index, sample.get()))
        {
            printError("Transcoder push", transcoder->error());
            return false;
        }
    }

    if (!transcoder->flush())
    {
        printError("Transcoder flush", transcoder->error());
        return false;
    }

    cout << "Output file: " << opt.output_file << endl;

    transcoder->close();

    return true;
}

Complete C++ Code

Here’s the complete working example that demonstrates HEVC / H.265 access-unit decoding using AVBlocks. This code combines all the previous snippets into a functional program that can be compiled and run.

#include <iostream>
#include <string>
#include <filesystem>
#include <unistd.h>

#include <primo/avblocks/avb.h>

#include <primo/platform/reference++.h>
#include <primo/platform/error_facility.h>
#include <primo/platform/ustring.h>

#include "options.h"
#include "util.h"

using namespace std;
using namespace primo::avblocks;
using namespace primo::codecs;

namespace fs = std::filesystem;
namespace av = primo::avblocks;
namespace pc = primo::codecs;

void printError(const char* action, const primo::error::ErrorInfo* e)
{
	if (action)
	{
		cout << action << ": ";
	}

	if (primo::error::ErrorFacility::Success == e->facility())
	{
		cout << "Success" << endl;
		return;
	}

	if (e->message())
	{
		cout << primo::ustring(e->message()) << " ";
	}

	cout << "(facility:" << e->facility() << " error:" << e->code() << ")" << endl;
}

MediaSocket* outputSocket(Options& opt, MediaSocket* inSocket)
{
    auto outSocket = primo::make_ref(Library::createMediaSocket());
    auto outPin = primo::make_ref(Library::createMediaPin());
    auto outVsi = primo::make_ref(Library::createVideoStreamInfo());

    outVsi->setColorFormat(opt.output_color.Id == ColorFormat::Unknown ? ColorFormat::YUV420 : opt.output_color.Id);

    if (opt.frame_size.height > 0)
        outVsi->setFrameHeight(opt.frame_size.height);

    if (opt.frame_size.width > 0)
        outVsi->setFrameWidth(opt.frame_size.width);

    if (opt.fps > 0)
        outVsi->setFrameRate(opt.fps);

    outVsi->setStreamType(StreamType::UncompressedVideo);
    outVsi->setScanType(ScanType::Progressive);

    outPin->setStreamInfo(outVsi.get());

    outSocket->pins()->add(outPin.get());

    if (opt.output_file.empty())
    {
        int yuv_width, yuv_height;

        if (opt.frame_size.height > 0 && opt.frame_size.width > 0)
        {
            yuv_width = opt.frame_size.width;
            yuv_height = opt.frame_size.height;
        }
        else
        {
            VideoStreamInfo* vsi = (VideoStreamInfo*)inSocket->pins()->at(0)->streamInfo();
            yuv_height = vsi->frameHeight();
            yuv_width = vsi->frameWidth();
        }

        // build output file name "path/decoded_wxh.yuv"
        fs::path output(getExeDir() + "/../../output/dec_hevc_au");
        fs::create_directories(output);

        ostringstream s;
        s << output.c_str() << "/decoded_" << yuv_width << "x" << yuv_height << ".yuv";
        opt.output_file = s.str();
    }

    outSocket->setFile(primo::ustring(opt.output_file));
    outSocket->setStreamType(StreamType::UncompressedVideo);

    return outSocket.release();
}

bool setTranscoder(Transcoder* transcoder, string imgFile, Options& opt)
{
    auto info = primo::make_ref(Library::createMediaInfo());
    info->inputs()->at(0)->setFile(primo::ustring(imgFile));

    if (!info->open())
    {
        printError("MediaInfo open", info->error());
        return false;
    }

    // prepare input socket
    auto inSocket = primo::make_ref(Library::createMediaSocket(info.get()));
    inSocket->setFile(NULL);
    inSocket->setStream(NULL);

    // prepare output socket
    auto outSocket = primo::make_ref(outputSocket(opt, inSocket.get()));

    transcoder->inputs()->add(inSocket.get());
    transcoder->outputs()->add(outSocket.get());

    deleteFile(opt.output_file.c_str());

    if (!transcoder->open())
    {
        printError("Transcoder open", transcoder->error());
        return false;
    }

    return true;
}

bool transcode(Options& opt)
{
    auto transcoder = primo::make_ref(Library::createTranscoder());
    transcoder->setAllowDemoMode(true);

    bool decoderInitialized = false;
    int32_t index = 0;

    for (int i = 0; ; i++)
    {
        char imgFile[PATH_MAX];
        string pattern = "au_%04d.h265";
        string imgPath = opt.input_dir + "/" + pattern;
        snprintf(imgFile, PATH_MAX, imgPath.c_str(), i);

        if (!decoderInitialized)
        {
            if (!setTranscoder(transcoder.get(), imgFile, opt))
                return false;

            decoderInitialized = true;
        }

        auto sample = primo::make_ref(Library::createMediaSample());

        vector<uint8_t> inputData = readFileBytes(imgFile);
        if (inputData.size() <= 0)
            break;

        auto buffer = primo::make_ref(Library::createMediaBuffer());
        buffer->attach(inputData.data(), inputData.size(), true);
        sample->setBuffer(buffer.get());

        if (!transcoder->push(index, sample.get()))
        {
            printError("Transcoder push", transcoder->error());
            return false;
        }
    }

    if (!transcoder->flush())
    {
        printError("Transcoder flush", transcoder->error());
        return false;
    }

    cout << "Output file: " << opt.output_file << endl;

    transcoder->close();

    return true;
}

int main(int argc, char* argv[])
{
    Options opt;

    switch(prepareOptions(opt, argc, argv))
    {
        case Command: return 0;
        case Error:	return 1;
        case Parsed: break;
    }

    primo::avblocks::Library::initialize();

    // set your license string
    // primo::avblocks::Library::setLicense("PRIMO-LICENSE");

    bool transcodeResult = transcode(opt);

    primo::avblocks::Library::shutdown();

    return transcodeResult ? 0 : 1;
}

How to Run

See the build instructions for macOS and the dec_hevc_au example for details.

Command Line

./dec_hevc_au --input <directory> [--output <file>] [--frame <width>x<height>] [--rate <fps>] [--color <COLOR>] [--colors]

Examples

List options:

./bin/x64/dec_hevc_au --help
Usage: dec_hevc_au --input <directory> [--output <file>] [--frame <width>x<height>] [--rate <fps>] [--color <COLOR>] [--colors]
  -h,    --help
  -i,    --input    input directory (contains sequence of compressed file)
  -o,    --output   output YUV file
  -r,    --rate     frame rate
  -c,    --color    output color format. Use --colors to list all supported color
                    formats
         --colors   list COLOR formats

The following command reads H.265 access-unit files under the assets/vid/foreman_qcif.h265.au/ directory and decodes them to raw video frames. The decoded video is written to output/dec_hevc_au/decoded_352x288.yuv:

mkdir -p ./output/dec_hevc_au

./bin/x64/dec_hevc_au \
  --input ./assets/vid/foreman_qcif.h265.au \
  --output ./output/dec_hevc_au/decoded_352x288.yuv \
  --color yuv420