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