HEVC / H.265 Annex B Parser¶
This article explains how you can use TTranscoder::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. The TLibrary object shuts the library down and cleans up resources when it 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_h265_stream(opt.input_file, opt.output_dir) ? 0 : 1;
}
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.
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)
};
static NALUType nal_unit_type(uint8_t first_byte)
{
// nal_unit_type occupies bits [6:1] of the first header byte
return static_cast<NALUType>((first_byte >> 1) & 0x3F);
}
#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(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);
}
Print NAL Unit Headers¶
This function reads the HEVC NAL unit type from the first byte of the two-byte NAL unit header and prints it.
static void print_nalu_header(const uint8_t* data)
{
// HEVC NAL unit header is 2 bytes; nal_unit_type is in the first byte
cout << left << " " << setw(12) << nal_unit_type(*data) << "\n";
}
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.
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.265 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 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.
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().streamType(StreamType::H265)))
)
.open();
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.
static void write_au_file(const string& outputDir, int au_index, TMediaBuffer& buffer)
{
ostringstream name;
name << outputDir << "/au_" << setw(4) << setfill('0') << au_index << ".h265";
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. 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 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 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 <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.265/HEVC spec, ITU-T H.265 Table 7-1)
// -----------------------------------------------------------------------
/*
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
*/
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)
};
static NALUType nal_unit_type(uint8_t first_byte)
{
// nal_unit_type occupies bits [6:1] of the first header byte
return static_cast<NALUType>((first_byte >> 1) & 0x3F);
}
#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(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);
}
static void print_nalu_header(const uint8_t* data)
{
// HEVC NAL unit header is 2 bytes; nal_unit_type is in the first byte
cout << left << " " << setw(12) << nal_unit_type(*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 << ".h265";
ofstream file(name.str(), ios::binary | ios::trunc);
if (file)
file.write(reinterpret_cast<const char*>(buffer.data()), buffer.dataSize());
}
// -----------------------------------------------------------------------
bool parse_h265_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().streamType(StreamType::H265)))
)
.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_h265_stream(opt.input_file, opt.output_dir) ? 0 : 1;
}
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