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 Python sample.
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. After the command line options are parsed, the program initializes the library, parses the stream and shuts the library down.
Library.initialize()
# Set license information. Without this AVBlocks runs in Demo mode.
# Library.set_license("<license-string>")
result = parse_h265_stream(input_file, output_dir)
Library.shutdown()
if result:
print(f"\nSuccessfully parsed input file: {input_file}")
print(f"Output directory: {output_dir}")
sys.exit(0 if result else 2)
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)
NALU_TYPES = {
0: "TRAIL_N",
1: "TRAIL_R",
2: "TSA_N",
3: "TSA_R",
4: "STSA_N",
5: "STSA_R",
6: "RADL_N",
7: "RADL_R",
8: "RASL_N",
9: "RASL_R",
16: "BLA_W_LP",
17: "BLA_W_RADL",
18: "BLA_N_LP",
19: "IDR_W_RADL",
20: "IDR_N_LP",
21: "CRA_NUT",
32: "VPS_NUT",
33: "SPS_NUT",
34: "PPS_NUT",
35: "AUD_NUT",
36: "EOS_NUT",
37: "EOB_NUT",
38: "FD_NUT",
39: "PREFIX_SEI",
40: "SUFFIX_SEI",
}
def get_nalu_type(first_byte: int) -> int:
"""Get NAL unit type from first header byte.
nal_unit_type occupies bits [6:1] of the first header byte.
"""
return (first_byte >> 1) & 0x3F
def get_nalu_type_name(nalu_type: int) -> str:
"""Get NAL unit type name."""
return NALU_TYPES.get(nalu_type, f"RSV_{nalu_type}")
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.
def print_nalu_header(byte: int):
"""Print HEVC NAL unit header information."""
nalu_type = get_nalu_type(byte)
print(f" {get_nalu_type_name(nalu_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.
def print_nalus(data: bytes):
"""Parse and print NAL units from an access unit buffer."""
pos = 0
size = len(data)
while size > 1:
# Check for 3-byte start code prefix (0x000001)
if size >= 3 and data[pos] == 0 and data[pos + 1] == 0 and data[pos + 2] == 1:
if size >= 4:
print_nalu_header(data[pos + 3])
pos += 3
size -= 3
# Check for 4-byte start code prefix (0x00000001)
elif size >= 4 and data[pos] == 0 and data[pos + 1] == 0 and data[pos + 2] == 0 and data[pos + 3] == 1:
if size >= 5:
print_nalu_header(data[pos + 4])
pos += 4
size -= 4
else:
pos += 1
size -= 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.
in_socket = MediaSocket()
in_socket.file = input_file
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.
# Create an output socket with one video pin configured for H.265
out_pin = MediaPin()
out_vsi = VideoStreamInfo()
out_vsi.stream_type = StreamType.H265
out_pin.stream_info = out_vsi
out_socket = MediaSocket()
out_socket.pins.add(out_pin)
with Transcoder() as transcoder:
transcoder.inputs.add(in_socket)
transcoder.outputs.add(out_socket)
res = transcoder.open()
print_error("transcoder open", transcoder.error)
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.
def write_au_file(output_dir: str, au_index: int, data: bytes):
"""Write an access unit to a file."""
filepath = os.path.join(output_dir, f"au_{au_index:04d}.h265")
with open(filepath, 'wb') as f:
f.write(data)
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.
au_index = 0
sample = MediaSample()
while True:
res, _ = transcoder.pull(sample)
if not res:
break
# Each call to transcoder.pull returns one Access Unit.
# The Access Unit may contain one or more NAL units.
au_data = bytes(sample.buffer.data)
print(f"AU #{au_index}, {len(au_data)} bytes")
write_au_file(output_dir, au_index, au_data)
print_nalus(au_data)
au_index += 1
transcoder.close()
Complete Code¶
Here’s the complete working example that demonstrates HEVC / H.265 Annex B parsing using AVBlocks for Python.
"""
Dump H.265/HEVC access units.
This sample parses an H.265/HEVC elementary stream and dumps each access unit
to a separate file. It also prints the NAL unit types contained in each AU.
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
"""
import os
import sys
import shutil
import click
from avblocks import (
Library, Transcoder, MediaSocket, MediaPin, MediaSample,
VideoStreamInfo, StreamType, ErrorFacility
)
# Network Abstraction Layer Unit Type Definitions per H.265/HEVC spec (ITU-T H.265 Table 7-1)
NALU_TYPES = {
0: "TRAIL_N",
1: "TRAIL_R",
2: "TSA_N",
3: "TSA_R",
4: "STSA_N",
5: "STSA_R",
6: "RADL_N",
7: "RADL_R",
8: "RASL_N",
9: "RASL_R",
16: "BLA_W_LP",
17: "BLA_W_RADL",
18: "BLA_N_LP",
19: "IDR_W_RADL",
20: "IDR_N_LP",
21: "CRA_NUT",
32: "VPS_NUT",
33: "SPS_NUT",
34: "PPS_NUT",
35: "AUD_NUT",
36: "EOS_NUT",
37: "EOB_NUT",
38: "FD_NUT",
39: "PREFIX_SEI",
40: "SUFFIX_SEI",
}
def get_nalu_type(first_byte: int) -> int:
"""Get NAL unit type from first header byte.
nal_unit_type occupies bits [6:1] of the first header byte.
"""
return (first_byte >> 1) & 0x3F
def get_nalu_type_name(nalu_type: int) -> str:
"""Get NAL unit type name."""
return NALU_TYPES.get(nalu_type, f"RSV_{nalu_type}")
def print_nalu_header(byte: int):
"""Print HEVC NAL unit header information."""
nalu_type = get_nalu_type(byte)
print(f" {get_nalu_type_name(nalu_type)}")
def print_nalus(data: bytes):
"""Parse and print NAL units from an access unit buffer."""
pos = 0
size = len(data)
while size > 1:
# Check for 3-byte start code prefix (0x000001)
if size >= 3 and data[pos] == 0 and data[pos + 1] == 0 and data[pos + 2] == 1:
if size >= 4:
print_nalu_header(data[pos + 3])
pos += 3
size -= 3
# Check for 4-byte start code prefix (0x00000001)
elif size >= 4 and data[pos] == 0 and data[pos + 1] == 0 and data[pos + 2] == 0 and data[pos + 3] == 1:
if size >= 5:
print_nalu_header(data[pos + 4])
pos += 4
size -= 4
else:
pos += 1
size -= 1
def write_au_file(output_dir: str, au_index: int, data: bytes):
"""Write an access unit to a file."""
filepath = os.path.join(output_dir, f"au_{au_index:04d}.h265")
with open(filepath, 'wb') as f:
f.write(data)
def delete_directory(dir_path: str):
"""Delete a directory if it exists."""
try:
if os.path.exists(dir_path):
shutil.rmtree(dir_path)
except OSError:
pass
def make_dir(dir_path: str) -> bool:
"""Create directory if it doesn't exist."""
try:
if not os.path.exists(dir_path):
os.makedirs(dir_path)
return True
except OSError:
return False
def print_error(action: str, error) -> None:
"""Print error information."""
if action:
print(f"{action}: ", end="")
if error.facility == ErrorFacility.Success:
print("Success")
return
print(f"{error.message or ''}, facility:{error.facility} code:{error.code} hint:{error.hint or ''}")
def parse_h265_stream(input_file: str, output_dir: str) -> bool:
"""Parse H.265 stream and dump access units."""
delete_directory(output_dir)
in_socket = MediaSocket()
in_socket.file = input_file
# Create an output socket with one video pin configured for H.265
out_pin = MediaPin()
out_vsi = VideoStreamInfo()
out_vsi.stream_type = StreamType.H265
out_pin.stream_info = out_vsi
out_socket = MediaSocket()
out_socket.pins.add(out_pin)
with Transcoder() as transcoder:
transcoder.inputs.add(in_socket)
transcoder.outputs.add(out_socket)
res = transcoder.open()
print_error("transcoder open", transcoder.error)
if not res:
return False
if not make_dir(output_dir):
print(f"cannot create output directory: {output_dir}")
return False
au_index = 0
sample = MediaSample()
while True:
res, _ = transcoder.pull(sample)
if not res:
break
# Each call to transcoder.pull returns one Access Unit.
# The Access Unit may contain one or more NAL units.
au_data = bytes(sample.buffer.data)
print(f"AU #{au_index}, {len(au_data)} bytes")
write_au_file(output_dir, au_index, au_data)
print_nalus(au_data)
au_index += 1
transcoder.close()
return True
@click.command()
@click.option('-i', '--input', 'input_file',
help='Input file (HEVC/H.265)',
type=click.Path(exists=True))
@click.option('-o', '--output', 'output_dir',
help='Output directory',
type=click.Path())
def main(input_file: str, output_dir: str):
"""Dump H.265/HEVC access units."""
# Set default options if not provided
if not input_file or not output_dir:
script_dir = os.path.dirname(os.path.abspath(__file__))
if not input_file:
input_file = os.path.join(script_dir, "../../assets/vid/foreman_qcif.h265")
if not output_dir:
output_dir = os.path.join(script_dir, "../../output/dump_hevc_au")
print("Using default options:")
print(f" --input {input_file}")
print(f" --output {output_dir}")
print()
# Validate options
print(f"--input: {input_file}")
print(f"--output: {output_dir}")
Library.initialize()
# Set license information. Without this AVBlocks runs in Demo mode.
# Library.set_license("<license-string>")
result = parse_h265_stream(input_file, output_dir)
Library.shutdown()
if result:
print(f"\nSuccessfully parsed input file: {input_file}")
print(f"Output directory: {output_dir}")
sys.exit(0 if result else 2)
if __name__ == '__main__':
main()
How to Run¶
See the Python sample for details. The samples are installed as command-line tools when you install the package from the repository root with uv pip install --editable . (see Installation).
Command Line¶
dump-hevc-au --input <h265 file> --output <folder>
Examples¶
List options:
dump-hevc-au --help
Usage: dump-hevc-au [OPTIONS]
Dump H.265/HEVC access units.
Options:
-i, --input PATH Input file (HEVC/H.265)
-o, --output PATH Output directory
--help Show this message and exit.
Parse the H.265 file ./assets/vid/foreman_qcif.h265 and dump Access Units to ./output/dump_hevc_au/:
# Linux and macOS
mkdir -p ./output/dump_hevc_au
dump-hevc-au \
--input ./assets/vid/foreman_qcif.h265 \
--output ./output/dump_hevc_au
# Windows
New-Item -ItemType Directory -Force -Path ./output/dump_hevc_au
dump-hevc-au `
--input ./assets/vid/foreman_qcif.h265 `
--output ./output/dump_hevc_au