HEVC / H.265 Annex B Parser

This article explains how you can use Transcoder::pull to parse an HEVC / H.265 elementary stream and extract access units (AU).

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

Linux and Windows samples are also available:

Source Video

As video input we use the foreman_qcif.h265 file from the AVBlocks Assets archive. After downloading and unzipping you will find foreman_qcif.h265 in the vid subdirectory.

Code

This code takes an H.265 / HEVC Annex B elementary stream, extracts each access unit, writes each access unit to a separate .h265 file, and prints the NAL (Network Abstraction Layer) unit type found in each access unit.

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;
        default:      break;
    }

    Library::initialize();

    // set your license string
    // Library::setLicense("avblocks-license-xml");

    bool parseResult = parse_h265_stream(opt.input_file, opt.output_dir);

    Library::shutdown();

    if (!parseResult)
        return 1;

    cout << "\nSuccessfully parsed input file: " << opt.input_file << "\n";
    cout << "Output directory: " << opt.output_dir << "\n\n";
    return 0;
}

Define NAL Unit Types

HEVC uses NAL units to structure the elementary stream. Each HEVC NAL unit has a two-byte header. The nal_unit_type value occupies bits 6 through 1 of the first header byte.

// Network Abstraction Layer Unit Type Definitions per H.265/HEVC spec (ITU-T H.265 Table 7-1)
enum NALUType
{
    TRAIL_N      = 0,   // Trailing picture, non-reference
    TRAIL_R      = 1,   // Trailing picture, reference
    TSA_N        = 2,   // Temporal sub-layer access, non-reference
    TSA_R        = 3,   // Temporal sub-layer access, reference
    STSA_N       = 4,   // Step-wise temporal sub-layer access, non-reference
    STSA_R       = 5,   // Step-wise temporal sub-layer access, reference
    RADL_N       = 6,   // Random access decodable leading, non-reference
    RADL_R       = 7,   // Random access decodable leading, reference
    RASL_N       = 8,   // Random access skipped leading, non-reference
    RASL_R       = 9,   // Random access skipped leading, reference
    BLA_W_LP     = 16,  // Broken link access with leading pictures
    BLA_W_RADL   = 17,  // Broken link access with RADL pictures
    BLA_N_LP     = 18,  // Broken link access without leading pictures
    IDR_W_RADL   = 19,  // IDR with RADL pictures
    IDR_N_LP     = 20,  // IDR without leading pictures
    CRA_NUT      = 21,  // Clean random access
    VPS_NUT      = 32,  // Video parameter set
    SPS_NUT      = 33,  // Sequence parameter set
    PPS_NUT      = 34,  // Picture parameter set
    AUD_NUT      = 35,  // Access unit delimiter
    EOS_NUT      = 36,  // End of sequence
    EOB_NUT      = 37,  // End of bitstream
    FD_NUT       = 38,  // Filler data
    PREFIX_SEI   = 39,  // Supplemental enhancement information (prefix)
    SUFFIX_SEI   = 40,  // Supplemental enhancement information (suffix)
    UNSPEC       = 62,  // Unspecified
};

NALUType nal_unit_type(uint8_t first_byte)
{
    // nal_unit_type occupies bits [6:1] of the first header byte
    int type = (first_byte >> 1) & 0x3F;
    return static_cast<NALUType>(type);
}

Parse NAL Units from Access Unit

This function iterates through the raw bytes of an access unit buffer, looking for Annex B start code prefixes (0x000001 or 0x00000001). When a start code is found, it prints the NAL unit header and advances past the start code.

void print_nalus(MediaBuffer* buffer)
{
    // This parsing code assumes that MediaBuffer contains
    // a single Access Unit of one or more complete NAL Units
    while (buffer->dataSize() > 1)
    {
        int dataOffset = buffer->dataOffset();
        int dataSize   = buffer->dataSize();
        const uint8_t* data = buffer->data();

        // is this a NALU with a 3-byte start code prefix (0x000001)?
        if (dataSize >= 3 &&
            0x00 == data[0] &&
            0x00 == data[1] &&
            0x01 == data[2])
        {
            if (dataSize >= 4)
                print_nalu_header(data + 3);

            buffer->setData(dataOffset + 3, dataSize - 3);
        }
        // OR is this a NALU with a 4-byte start code prefix (0x00000001)?
        else if (dataSize >= 4 &&
                 0x00 == data[0] &&
                 0x00 == data[1] &&
                 0x00 == data[2] &&
                 0x01 == data[3])
        {
            if (dataSize >= 5)
                print_nalu_header(data + 4);

            buffer->setData(dataOffset + 4, dataSize - 4);
        }
        else
        {
            buffer->setData(dataOffset + 1, dataSize - 1);
        }

        // NOTE: Some NALUs may have a trailing zero byte. The `while`
        // condition `buffer->dataSize() > 1` will effectively
        // skip the trailing zero byte.
    }
}

Create Input Socket

We create an input socket that points to the H.265 input file. The socket tells the transcoder where to read the encoded video stream from.

auto inSocket = primo::make_ref(Library::createMediaSocket());
inSocket->setFile(primo::ustring(inputFile));

Configure Transcoder

We create a transcoder with an output socket configured for H.265. The output socket does not write to a file directly; it lets the application pull encoded access units from the transcoder.

auto outStreamInfo = primo::make_ref(Library::createVideoStreamInfo());
outStreamInfo->setStreamType(StreamType::H265);

auto outPin = primo::make_ref(Library::createMediaPin());
outPin->setStreamInfo(outStreamInfo.get());

auto outSocket = primo::make_ref(Library::createMediaSocket());
outSocket->pins()->add(outPin.get());

auto transcoder = primo::make_ref(Library::createTranscoder());
transcoder->inputs()->add(inSocket.get());
transcoder->outputs()->add(outSocket.get());

auto res = transcoder->open();
printError("transcoder open", transcoder->error());

if (!res)
    return false;

Write Access Unit to File

Each access unit is written to a separate file named au_####.h265, where #### is a zero-padded index. The file is opened in binary mode to preserve the raw H.265 data.

void write_au_file(string outputDir, int au_index, MediaBuffer* buffer)
{
    ostringstream file_name;
    file_name << outputDir << "/au_" << setw(4) << setfill('0') << au_index << ".h265";
    ofstream file(file_name.str(), ios::binary | ios::trunc);
    if (file)
    {
        file.write((const char*)buffer->data(), buffer->dataSize());
    }
}

Pull Access Units

This is the main parsing loop. Each call to Transcoder::pull returns one access unit. For each access unit, the sample prints its index and size, writes it to a separate file, and prints the NAL unit types within it.

int32_t inputIndex = 0;
auto accessUnit = primo::make_ref(Library::createMediaSample());

if (!makeDir(outputDir))
{
    cout << "cannot create output directory: " << outputDir << endl;
    return false;
}

int au_index = 0;
while (transcoder->pull(inputIndex, accessUnit.get()))
{
    // Each call to Transcoder::pull returns one Access Unit.
    // The Access Unit may contain one or more NAL units.

    auto au_buffer = accessUnit->buffer();
    cout << "AU #" << au_index << ", " << au_buffer->dataSize() << " bytes" << endl;
    write_au_file(outputDir, au_index, au_buffer);
    print_nalus(au_buffer);
    ++au_index;
}
transcoder->close();
return true;

Complete C++ Code

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

#include <iostream>
#include <iomanip>
#include <fstream>
#include <string>
#include <map>
#include <memory.h>
#include <filesystem>

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

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

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

namespace fs = std::filesystem;

/*
 Access Unit: one picture (VPS/SPS/PPS parameters + all slices)
 The elementary stream may contain optional access unit delimiters (AUD).
 A picture may consist of one or more slices.

 HEVC NAL unit header (2 bytes):
   forbidden_zero_bit    (1 bit)
   nal_unit_type         (6 bits)
   nuh_layer_id          (6 bits)
   nuh_temporal_id_plus1 (3 bits)

 nal_unit_type = (first_byte >> 1) & 0x3F
*/

// Network Abstraction Layer Unit Type Definitions per H.265/HEVC spec (ITU-T H.265 Table 7-1)
enum NALUType
{
    TRAIL_N      = 0,   // Trailing picture, non-reference
    TRAIL_R      = 1,   // Trailing picture, reference
    TSA_N        = 2,   // Temporal sub-layer access, non-reference
    TSA_R        = 3,   // Temporal sub-layer access, reference
    STSA_N       = 4,   // Step-wise temporal sub-layer access, non-reference
    STSA_R       = 5,   // Step-wise temporal sub-layer access, reference
    RADL_N       = 6,   // Random access decodable leading, non-reference
    RADL_R       = 7,   // Random access decodable leading, reference
    RASL_N       = 8,   // Random access skipped leading, non-reference
    RASL_R       = 9,   // Random access skipped leading, reference
    BLA_W_LP     = 16,  // Broken link access with leading pictures
    BLA_W_RADL   = 17,  // Broken link access with RADL pictures
    BLA_N_LP     = 18,  // Broken link access without leading pictures
    IDR_W_RADL   = 19,  // IDR with RADL pictures
    IDR_N_LP     = 20,  // IDR without leading pictures
    CRA_NUT      = 21,  // Clean random access
    VPS_NUT      = 32,  // Video parameter set
    SPS_NUT      = 33,  // Sequence parameter set
    PPS_NUT      = 34,  // Picture parameter set
    AUD_NUT      = 35,  // Access unit delimiter
    EOS_NUT      = 36,  // End of sequence
    EOB_NUT      = 37,  // End of bitstream
    FD_NUT       = 38,  // Filler data
    PREFIX_SEI   = 39,  // Supplemental enhancement information (prefix)
    SUFFIX_SEI   = 40,  // Supplemental enhancement information (suffix)
    UNSPEC       = 62,  // Unspecified
};

NALUType nal_unit_type(uint8_t first_byte)
{
    // nal_unit_type occupies bits [6:1] of the first header byte
    int type = (first_byte >> 1) & 0x3F;
    return static_cast<NALUType>(type);
}

#define MAP_ENUM_VALUE(p) strings[p] = #p
std::ostream& operator<<(std::ostream& out, const NALUType value)
{
    static std::map<NALUType, std::string> strings;
    if (strings.empty())
    {
        MAP_ENUM_VALUE(TRAIL_N);
        MAP_ENUM_VALUE(TRAIL_R);
        MAP_ENUM_VALUE(TSA_N);
        MAP_ENUM_VALUE(TSA_R);
        MAP_ENUM_VALUE(STSA_N);
        MAP_ENUM_VALUE(STSA_R);
        MAP_ENUM_VALUE(RADL_N);
        MAP_ENUM_VALUE(RADL_R);
        MAP_ENUM_VALUE(RASL_N);
        MAP_ENUM_VALUE(RASL_R);
        MAP_ENUM_VALUE(BLA_W_LP);
        MAP_ENUM_VALUE(BLA_W_RADL);
        MAP_ENUM_VALUE(BLA_N_LP);
        MAP_ENUM_VALUE(IDR_W_RADL);
        MAP_ENUM_VALUE(IDR_N_LP);
        MAP_ENUM_VALUE(CRA_NUT);
        MAP_ENUM_VALUE(VPS_NUT);
        MAP_ENUM_VALUE(SPS_NUT);
        MAP_ENUM_VALUE(PPS_NUT);
        MAP_ENUM_VALUE(AUD_NUT);
        MAP_ENUM_VALUE(EOS_NUT);
        MAP_ENUM_VALUE(EOB_NUT);
        MAP_ENUM_VALUE(FD_NUT);
        MAP_ENUM_VALUE(PREFIX_SEI);
        MAP_ENUM_VALUE(SUFFIX_SEI);
    }

    auto it = strings.find(value);
    if (it != strings.end())
        return out << it->second;

    return out << "RSV_" << static_cast<int>(value);
}

void print_nalu_header(const uint8_t* data)
{
    // HEVC NAL unit header is 2 bytes; nal_unit_type is in the first byte
    std::cout << left << "  "
              << setw(12) << nal_unit_type(*data)
              << std::endl;
}

void print_nalus(MediaBuffer* buffer)
{
    // This parsing code assumes that MediaBuffer contains
    // a single Access Unit of one or more complete NAL Units
    while (buffer->dataSize() > 1)
    {
        int dataOffset = buffer->dataOffset();
        int dataSize   = buffer->dataSize();
        const uint8_t* data = buffer->data();

        // is this a NALU with a 3-byte start code prefix (0x000001)?
        if (dataSize >= 3 &&
            0x00 == data[0] &&
            0x00 == data[1] &&
            0x01 == data[2])
        {
            if (dataSize >= 4)
                print_nalu_header(data + 3);

            buffer->setData(dataOffset + 3, dataSize - 3);
        }
        // OR is this a NALU with a 4-byte start code prefix (0x00000001)?
        else if (dataSize >= 4 &&
                 0x00 == data[0] &&
                 0x00 == data[1] &&
                 0x00 == data[2] &&
                 0x01 == data[3])
        {
            if (dataSize >= 5)
                print_nalu_header(data + 4);

            buffer->setData(dataOffset + 4, dataSize - 4);
        }
        else
        {
            buffer->setData(dataOffset + 1, dataSize - 1);
        }

        // NOTE: Some NALUs may have a trailing zero byte. The `while`
        // condition `buffer->dataSize() > 1` will effectively
        // skip the trailing zero byte.
    }
}

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;
}

void write_au_file(string outputDir, int au_index, MediaBuffer* buffer)
{
    ostringstream file_name;
    file_name << outputDir << "/au_" << setw(4) << setfill('0') << au_index << ".h265";
    ofstream file(file_name.str(), ios::binary | ios::trunc);
    if (file)
    {
        file.write((const char*)buffer->data(), buffer->dataSize());
    }
}

bool parse_h265_stream(string inputFile, string outputDir)
{
    // Create an input socket from file
    auto inSocket = primo::make_ref(Library::createMediaSocket());
    inSocket->setFile(primo::ustring(inputFile));

    // Create an output socket with one video pin configured for H.265
    auto outStreamInfo = primo::make_ref(Library::createVideoStreamInfo());
    outStreamInfo->setStreamType(StreamType::H265);

    auto outPin = primo::make_ref(Library::createMediaPin());
    outPin->setStreamInfo(outStreamInfo.get());

    auto outSocket = primo::make_ref(Library::createMediaSocket());
    outSocket->pins()->add(outPin.get());

    // Create Transcoder
    auto transcoder = primo::make_ref(Library::createTranscoder());
    transcoder->inputs()->add(inSocket.get());
    transcoder->outputs()->add(outSocket.get());

    auto res = transcoder->open();
    printError("transcoder open", transcoder->error());

    if (!res)
        return false;

    int32_t inputIndex = 0;
    auto accessUnit = primo::make_ref(Library::createMediaSample());

    if (!makeDir(outputDir))
    {
        cout << "cannot create output directory: " << outputDir << endl;
        return false;
    }

    int au_index = 0;
    while (transcoder->pull(inputIndex, accessUnit.get()))
    {
        // Each call to Transcoder::pull returns one Access Unit.
        // The Access Unit may contain one or more NAL units.

        auto au_buffer = accessUnit->buffer();
        cout << "AU #" << au_index << ", " << au_buffer->dataSize() << " bytes" << endl;
        write_au_file(outputDir, au_index, au_buffer);
        print_nalus(au_buffer);
        ++au_index;
    }
    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;
        default:      break;
    }

    Library::initialize();

    // set your license string
    // Library::setLicense("avblocks-license-xml");

    bool parseResult = parse_h265_stream(opt.input_file, opt.output_dir);

    Library::shutdown();

    if (!parseResult)
        return 1;

    cout << "\nSuccessfully parsed input file: " << opt.input_file << "\n";
    cout << "Output directory: " << opt.output_dir << "\n\n";
    return 0;
}

How to Run

Use --input to specify the HEVC / H.265 input file and --output to specify the folder where the access units will be written.

./dump_hevc_au --input <h265-file> --output <folder>

or short form:

./dump_hevc_au -i <h265-file> -o <folder>

To list the available options:

./bin/x64/dump_hevc_au --help
Usage: dump_hevc_au --input <h265-file> --output <folder>
  -h,    --help
  -i,    --input    input file (HEVC/H.265)
  -o,    --output   output directory

The following command extracts HEVC access units from the foreman_qcif.h265 video and writes them to output/dump_hevc_au as separate au_####.h265 files:

mkdir -p ./output/dump_hevc_au

./bin/x64/dump_hevc_au \
  --input ./assets/vid/foreman_qcif.h265 \
  --output ./output/dump_hevc_au