AVC / H.264 Annex B Parser

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

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

Linux and Windows samples are also available:

Source Video

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

Code

This code takes an H.264 stream encoded according to ISO/IEC 14496 - 10 Annex B, extracts each access unit, writes them to separate files, and prints the NAL (Network Abstraction Layer) unit headers within 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. The TLibrary constructor initializes the library and loads the necessary codecs. The TLibrary destructor shuts the library down and releases any allocated memory, so the library is terminated when the TLibrary object goes out of scope at the end of main.

int main(int argc, char* argv[])
{
    Options opt;
    switch(prepareOptions(opt, argc, argv))
    {
        case Command: return 0;
        case Error:   return 1;
        case Parsed:  break;
    }

    TLibrary library;
    return parse_h264_stream(opt.input_file, opt.output_dir) ? 0 : 1;
}

Define NAL Unit Types

H.264 uses NAL (Network Abstraction Layer) units to structure the elementary stream. Each NAL unit has a type that identifies its purpose — whether it contains slice data, sequence parameters, picture parameters, or other metadata.

enum NALUType
{
    UNSPEC   = 0,
    SLICE    = 1,  // Coded slice of a non-IDR picture
    DPA      = 2,  // Coded slice data partition A
    DPB      = 3,  // Coded slice data partition B
    DPC      = 4,  // Coded slice data partition C
    IDR      = 5,  // Coded slice of an IDR picture
    SEI      = 6,  // Supplemental enhancement information
    SPS      = 7,  // Sequence parameter set
    PPS      = 8,  // Picture parameter set
    AUD      = 9,  // Access unit delimiter
    EOSEQ    = 10, // End of sequence
    EOSTREAM = 11, // End of stream
    FILL     = 12  // Filler data
};

enum NALUPriority
{
    DISPOSABLE = 0,
    LOW        = 1,
    HIGH       = 2,
    HIGHEST    = 3,
};

static NALUType     nal_unit_type    (uint8_t b) { return static_cast<NALUType>    (b & 0x1f); }
static NALUPriority nal_unit_ref_idc (uint8_t b) { return static_cast<NALUPriority>((b & 0x60) >> 5); }

#define MAP_ENUM_VALUE(p) strings[p] = #p

static ostream& operator<<(ostream& out, NALUType value)
{
    static map<NALUType, string> strings;
    if (strings.empty())
    {
        MAP_ENUM_VALUE(SLICE); MAP_ENUM_VALUE(DPA);    MAP_ENUM_VALUE(DPB);
        MAP_ENUM_VALUE(DPC);   MAP_ENUM_VALUE(IDR);    MAP_ENUM_VALUE(SEI);
        MAP_ENUM_VALUE(SPS);   MAP_ENUM_VALUE(PPS);    MAP_ENUM_VALUE(AUD);
        MAP_ENUM_VALUE(EOSEQ); MAP_ENUM_VALUE(EOSTREAM); MAP_ENUM_VALUE(FILL);
    }
    return out << strings[value];
}

static ostream& operator<<(ostream& out, NALUPriority value)
{
    static map<NALUPriority, string> strings;
    if (strings.empty())
    {
        MAP_ENUM_VALUE(DISPOSABLE); MAP_ENUM_VALUE(LOW);
        MAP_ENUM_VALUE(HIGH);       MAP_ENUM_VALUE(HIGHEST);
    }
    return out << strings[value];
}

Parse NAL Units from Access Unit

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

static void print_nalus(TMediaBuffer& buffer)
{
    while (buffer.dataSize() > 1)
    {
        int32_t        dataOffset = buffer.dataOffset();
        int32_t        dataSize   = buffer.dataSize();
        const uint8_t* data       = buffer.data();

        if (dataSize >= 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01)
        {
            print_nalu_header(data + 3);
            buffer.setData(dataOffset + 3, dataSize - 3);
        }
        else if (dataSize >= 4 && data[0] == 0x00 && data[1] == 0x00 &&
                 data[2] == 0x00 && data[3] == 0x01)
        {
            print_nalu_header(data + 4);
            buffer.setData(dataOffset + 4, dataSize - 4);
        }
        else
        {
            buffer.setData(dataOffset + 1, dataSize - 1);
        }
    }
}

Create Input Socket

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

TMediaSocket()
    .file(inputFile)

Configure Transcoder

We create a transcoder with an output socket that has a video pin. The output socket doesn’t write to a file — it just provides a place for the access units to be delivered.

TTranscoder transcoder;
transcoder
    .addInput(
        TMediaSocket()
            .file(inputFile)
    )
    .addOutput(
        // Pull-mode output: no file, just a video pin to receive AUs
        TMediaSocket()
            .addPin(TMediaPin().streamInfo(TVideoStreamInfo()))
    )
    .open();

Write Access Unit to File

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

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

Pull Access Units

This is the main parsing loop. Each call to TTranscoder::pull returns one access unit (a complete picture with all its slices). For each access unit, we print its index and size, write it to a separate file, and print the NAL unit headers within it.

int32_t outputIndex = 0;
TMediaSample accessUnit;

int au_index = 0;
while (transcoder.pull(outputIndex, accessUnit))
{
    auto buf = accessUnit.buffer();
    println("AU #{}, {} bytes", au_index, buf.dataSize());
    write_au_file(outputDir, au_index, buf);
    print_nalus(buf);
    ++au_index;
}

const auto error = transcoder.error();
if (!(error.facility() == primo::error::ErrorFacility::Codec &&
      error.code()     == primo::codecs::CodecError::EOS))
    printError("Transcoder pull", error);

transcoder.close();

Complete C++ Code

Here’s the complete working example that demonstrates H.264 Annex B parsing using AVBlocks. This code combines all the previous snippets into a functional program that can be compiled and run. The main function handles command-line argument parsing, initializes AVBlocks, parses the H.264 stream using pull, extracts access units to separate files, and properly shuts down the library before exiting.

#include <primo/avblocks/avb++.h>

#include <iostream>
#include <print>
#include <iomanip>
#include <fstream>
#include <map>

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

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

// -----------------------------------------------------------------------
// NAL unit parsing (H.264/AVC spec)
// -----------------------------------------------------------------------

enum NALUType
{
    UNSPEC   = 0,
    SLICE    = 1,  // Coded slice of a non-IDR picture
    DPA      = 2,  // Coded slice data partition A
    DPB      = 3,  // Coded slice data partition B
    DPC      = 4,  // Coded slice data partition C
    IDR      = 5,  // Coded slice of an IDR picture
    SEI      = 6,  // Supplemental enhancement information
    SPS      = 7,  // Sequence parameter set
    PPS      = 8,  // Picture parameter set
    AUD      = 9,  // Access unit delimiter
    EOSEQ    = 10, // End of sequence
    EOSTREAM = 11, // End of stream
    FILL     = 12  // Filler data
};

enum NALUPriority
{
    DISPOSABLE = 0,
    LOW        = 1,
    HIGH       = 2,
    HIGHEST    = 3,
};

static NALUType     nal_unit_type    (uint8_t b) { return static_cast<NALUType>    (b & 0x1f); }
static NALUPriority nal_unit_ref_idc (uint8_t b) { return static_cast<NALUPriority>((b & 0x60) >> 5); }

#define MAP_ENUM_VALUE(p) strings[p] = #p

static ostream& operator<<(ostream& out, NALUType value)
{
    static map<NALUType, string> strings;
    if (strings.empty())
    {
        MAP_ENUM_VALUE(SLICE); MAP_ENUM_VALUE(DPA);    MAP_ENUM_VALUE(DPB);
        MAP_ENUM_VALUE(DPC);   MAP_ENUM_VALUE(IDR);    MAP_ENUM_VALUE(SEI);
        MAP_ENUM_VALUE(SPS);   MAP_ENUM_VALUE(PPS);    MAP_ENUM_VALUE(AUD);
        MAP_ENUM_VALUE(EOSEQ); MAP_ENUM_VALUE(EOSTREAM); MAP_ENUM_VALUE(FILL);
    }
    return out << strings[value];
}

static ostream& operator<<(ostream& out, NALUPriority value)
{
    static map<NALUPriority, string> strings;
    if (strings.empty())
    {
        MAP_ENUM_VALUE(DISPOSABLE); MAP_ENUM_VALUE(LOW);
        MAP_ENUM_VALUE(HIGH);       MAP_ENUM_VALUE(HIGHEST);
    }
    return out << strings[value];
}

static void print_nalu_header(const uint8_t* data)
{
    cout << left << "  " << setw(8) << nal_unit_type(*data) << ": "
         << nal_unit_ref_idc(*data) << "\n";
}

static void print_nalus(TMediaBuffer& buffer)
{
    while (buffer.dataSize() > 1)
    {
        int32_t        dataOffset = buffer.dataOffset();
        int32_t        dataSize   = buffer.dataSize();
        const uint8_t* data       = buffer.data();

        if (dataSize >= 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01)
        {
            print_nalu_header(data + 3);
            buffer.setData(dataOffset + 3, dataSize - 3);
        }
        else if (dataSize >= 4 && data[0] == 0x00 && data[1] == 0x00 &&
                 data[2] == 0x00 && data[3] == 0x01)
        {
            print_nalu_header(data + 4);
            buffer.setData(dataOffset + 4, dataSize - 4);
        }
        else
        {
            buffer.setData(dataOffset + 1, dataSize - 1);
        }
    }
}

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

// -----------------------------------------------------------------------

bool parse_h264_stream(const string& inputFile, const string& outputDir)
{
    try {
        TTranscoder transcoder;
        transcoder
            .addInput(
                TMediaSocket()
                    .file(inputFile)
            )
            .addOutput(
                // Pull-mode output: no file, just a video pin to receive AUs
                TMediaSocket()
                    .addPin(TMediaPin().streamInfo(TVideoStreamInfo()))
            )
            .open();

        if (!makeDir(outputDir))
        {
            println(stderr, "Cannot create output directory: {}", outputDir);
            return false;
        }

        int32_t outputIndex = 0;
        TMediaSample accessUnit;

        int au_index = 0;
        while (transcoder.pull(outputIndex, accessUnit))
        {
            auto buf = accessUnit.buffer();
            println("AU #{}, {} bytes", au_index, buf.dataSize());
            write_au_file(outputDir, au_index, buf);
            print_nalus(buf);
            ++au_index;
        }

        const auto error = transcoder.error();
        if (!(error.facility() == primo::error::ErrorFacility::Codec &&
              error.code()     == primo::codecs::CodecError::EOS))
            printError("Transcoder pull", error);

        transcoder.close();
        return true;

    } catch (const TAVBlocksException& ex) {
        println(stderr, "AVBlocks error: {}", ex.what());
        return false;
    }
}

int main(int argc, char* argv[])
{
    Options opt;
    switch(prepareOptions(opt, argc, argv))
    {
        case Command: return 0;
        case Error:   return 1;
        case Parsed:  break;
    }

    TLibrary library;
    return parse_h264_stream(opt.input_file, opt.output_dir) ? 0 : 1;
}

How to Run

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

Command Line

./dump_avc_au --input <h264-file> --output <folder>

Examples

List options:

./bin/x64/dump_avc_au --help
Usage: dump_avc_au --input <h264-file> --output <folder>
  -h,    --help
  -i,    --input    input file (AVC/H.264)
  -o,    --output   output directory

The following command extracts the H.264 access units from the foreman_qcif.h264 video and writes them to the folder output/dump_avc_au as separate files (au_####.h264):

mkdir -p ./output/dump_avc_au

./bin/x64/dump_avc_au \
  --input ./assets/vid/foreman_qcif.h264 \
  --output ./output/dump_avc_au