AVC / H.264 Annex B Decoder (Pull)¶
This article explains how you can use TTranscoder::pull to decode an AVC / H.264 elementary stream.
The code snippets in this article are from the dec_avc_pull 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, and decodes it to raw uncompressed YUV frames.
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 decodeAvcPull(opt) ? 0 : 1;
}
Configure Output Socket¶
The decoder output socket defines the format of the decoded video data. In this case, we’re configuring it to output raw uncompressed YUV 4:2:0 progressive video. The socket represents the output destination, while the pin represents the specific video stream within that destination. The TVideoStreamInfo object specifies the video format parameters.
TMediaSocket()
.streamType(StreamType::UncompressedVideo)
.addPin(
TMediaPin()
.streamInfo(TVideoStreamInfo()
.streamType(StreamType::UncompressedVideo)
.colorFormat(ColorFormat::YUV420)
.scanType(ScanType::Progressive)
)
)
Create Output File¶
Before decoding, we open the output file for writing. The transcoder will fail if the output file already exists (by design), so we delete it first. The file is opened in binary mode since we’re writing raw YUV frame data.
// transcoder will fail if output exists (by design)
deleteFile(opt.outputFile.c_str());
std::ofstream outfile(opt.outputFile, std::ios_base::binary);
if (!outfile.is_open())
{
std::println(stderr, "Could not open file {}", opt.outputFile);
return false;
}
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(opt.inputFile)
Configure Transcoder¶
We create the transcoder, add the input and output sockets, and open it to prepare for decoding. The allowDemoMode(true) call allows the transcoder to work even without a valid license (useful for testing, but not recommended for production).
TTranscoder transcoder;
transcoder
.allowDemoMode(true)
.addInput(
TMediaSocket()
.file(opt.inputFile)
)
.addOutput(
// no file - decoded frames are pulled manually
TMediaSocket()
.streamType(StreamType::UncompressedVideo)
.addPin(
TMediaPin()
.streamInfo(TVideoStreamInfo()
.streamType(StreamType::UncompressedVideo)
.colorFormat(ColorFormat::YUV420)
.scanType(ScanType::Progressive)
)
)
)
.open();
Pull Decoded Frames¶
This is the main decoding loop. Each call to TTranscoder::pull returns one decoded raw YUV 4:2:0 frame, which is written directly to the output file. The loop continues until the transcoder returns false, which happens when all input has been processed.
int32_t outputIndex = 0;
int32_t frameIndex = 0;
TMediaSample yuvFrame;
while (transcoder.pull(outputIndex, yuvFrame))
{
// each call to Transcoder::pull returns a raw YUV 4:2:0 frame
auto buffer = yuvFrame.buffer();
outfile.write(reinterpret_cast<const char*>(buffer.data()), buffer.dataSize());
std::println("Frame Index: {}", frameIndex);
++frameIndex;
}
Check Results¶
After the pull loop finishes, we check the error code to determine if the decoding completed successfully (EOS - End of Stream) or if an actual error occurred. We then close the transcoder and print summary information.
const auto error = transcoder.error();
const bool success = error.facility() == primo::error::ErrorFacility::Codec &&
error.code() == primo::codecs::CodecError::EOS;
if (!success)
printError("Transcoder pull", error);
transcoder.close();
std::println("Output: {}", opt.outputFile);
std::println("Total frames: {}", frameIndex);
return success;
Complete C++ Code¶
Here’s the complete working example that demonstrates AVC/H.264 decoding 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, performs the decoding operation using pull, and properly shuts down the library before exiting.
#include <primo/avblocks/avb++.h>
#include <fstream>
#include <print>
#include "options.h"
#include "util.h"
using namespace primo::codecs;
using namespace primo::avblocks::modern;
bool decodeAvcPull(const Options& opt)
{
// transcoder will fail if output exists (by design)
deleteFile(opt.outputFile.c_str());
std::ofstream outfile(opt.outputFile, std::ios_base::binary);
if (!outfile.is_open())
{
std::println(stderr, "Could not open file {}", opt.outputFile);
return false;
}
try {
TTranscoder transcoder;
transcoder
.allowDemoMode(true)
.addInput(
TMediaSocket()
.file(opt.inputFile)
)
.addOutput(
// no file - decoded frames are pulled manually
TMediaSocket()
.streamType(StreamType::UncompressedVideo)
.addPin(
TMediaPin()
.streamInfo(TVideoStreamInfo()
.streamType(StreamType::UncompressedVideo)
.colorFormat(ColorFormat::YUV420)
.scanType(ScanType::Progressive)
)
)
)
.open();
// decode by pulling decoded samples
int32_t outputIndex = 0;
int32_t frameIndex = 0;
TMediaSample yuvFrame;
while (transcoder.pull(outputIndex, yuvFrame))
{
// each call to Transcoder::pull returns a raw YUV 4:2:0 frame
auto buffer = yuvFrame.buffer();
outfile.write(reinterpret_cast<const char*>(buffer.data()), buffer.dataSize());
std::println("Frame Index: {}", frameIndex);
++frameIndex;
}
const auto error = transcoder.error();
const bool success = error.facility() == primo::error::ErrorFacility::Codec &&
error.code() == primo::codecs::CodecError::EOS;
if (!success)
printError("Transcoder pull", error);
transcoder.close();
std::println("Output: {}", opt.outputFile);
std::println("Total frames: {}", frameIndex);
return success;
} catch (const TAVBlocksException& ex) {
std::println(stderr, "AVBlocks error: {}", ex.what());
return false;
} catch (const std::exception& ex) {
std::println(stderr, "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 decodeAvcPull(opt) ? 0 : 1;
}
How to Run¶
See the build instructions for macOS and the dec_avc_pull example for details.
Command Line¶
./dec_avc_pull --input <h264 file> --output <yuv file>
Examples¶
List options:
./bin/x64/dec_avc_pull --help
Usage: dec_avc_pull -i <h264 file> -o <yuv file>
-h, --help
--input H264 input file.
--output YUV output file.
The following command decodes an AVC/H.264 video from foreman_qcif.h264 to foreman_qcif.yuv:
mkdir -p ./output/dec_avc_pull
./bin/x64/dec_avc_pull \
--input ./assets/vid/foreman_qcif.h264 \
--output ./output/dec_avc_pull/foreman_qcif.yuv