AVC / H.264 Annex B Decoder (Pull)

This article explains how you can use Transcoder::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 Library::initialize() method sets up the internal state and loads necessary codecs. Always remember to call Library::shutdown() at the end of your program to properly clean up resources and release any allocated memory.

int main(int argc, char* argv[])
{
    Options opt;
    
    switch(prepareOptions(opt, argc, argv))
    {
        case Command: return 0;
        case Error:	return 1;
        case Parsed: break;
    }
    
    Library::initialize();
    
    bool decodeResult = decodeAvcPull(opt);
    
    Library::shutdown();
    
    return decodeResult ? 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 VideoStreamInfo object specifies the video format parameters.

primo::ref<MediaSocket> createOutputSocket(Options& opt)
{
    auto vsi = primo::make_ref(Library::createVideoStreamInfo());
    vsi->setStreamType(StreamType::UncompressedVideo);
    vsi->setColorFormat(ColorFormat::YUV420);
    vsi->setScanType(ScanType::Progressive);

    auto pin = primo::make_ref(Library::createMediaPin());
    pin->setStreamInfo(vsi.get());
    
    auto socket = primo::make_ref(Library::createMediaSocket());
    socket->setStreamType(StreamType::UncompressedVideo);
    socket->pins()->add(pin.get());
    
    return socket;
}

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(primo::ustring(opt.outputFile));

ofstream outfile(opt.outputFile, ios_base::binary);
if (!outfile.is_open())
{
    cout << "Could not open file " << opt.outputFile << endl;
    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.

auto inSocket = primo::make_ref(Library::createMediaSocket());
inSocket->setFile(primo::ustring(opt.inputFile));

Configure Transcoder

We create the transcoder, add the input and output sockets, and open it to prepare for decoding. The setAllowDemoMode(true) call allows the transcoder to work even without a valid license (useful for testing, but not recommended for production).

auto outSocket = createOutputSocket(opt);

auto transcoder = primo::make_ref(Library::createTranscoder());
transcoder->setAllowDemoMode(true);
transcoder->inputs()->add(inSocket.get());
transcoder->outputs()->add(outSocket.get());

if (!transcoder->open())
{
    printError("Transcoder::open", transcoder->error());
    return false;
}

Pull Decoded Frames

This is the main decoding loop. Each call to Transcoder::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 inputIndex = 0;
auto yuvFrame = primo::make_ref(Library::createMediaSample());
int32_t frameIndex = 0;

while (transcoder->pull(inputIndex, yuvFrame.get()))
{
    // Each call to Transcoder::pull returns a raw YUV 4:2:0 frame
    outfile.write((const char *)yuvFrame->buffer()->data(), yuvFrame->buffer()->dataSize());
    
    cout << "Frame Index: " << frameIndex << endl;
    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 primo::error::ErrorInfo* error = transcoder->error();
printStatus(error);

bool success = false;
if ((error->facility() == primo::error::ErrorFacility::Codec) &&
    (error->code() == primo::codecs::CodecError::EOS))
{
    // ok - end of stream
    success = true;
}

transcoder->close();

cout << "Output: " << opt.outputFile << endl;
cout << "Total frames: " << frameIndex << endl;

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 <unistd.h>

#include <iostream>
#include <string>
#include <fstream>
#include <filesystem>

#include <primo/platform/reference++.h>
#include <primo/platform/error_facility.h>
#include <primo/platform/ustring.h>

#include <primo/avblocks/avb.h>

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

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

namespace fs = std::filesystem;
namespace av = primo::avblocks;
namespace pc = primo::codecs;

void printStatus(const primo::error::ErrorInfo* e)
{
    if (primo::error::ErrorFacility::Success == e->facility())
    {
        cout << "Success";
        
    }
    else
    {
        if (e->message())
        {
            cout << primo::ustring(e->message()) << " ";
        }
        
        cout << "(facility:" << e->facility() << " error:" << e->code() << ")" << endl;
    }
    
    cout << endl;
}

primo::ref<MediaSocket> createOutputSocket(Options& opt)
{
    auto vsi = primo::make_ref(Library::createVideoStreamInfo());
    vsi->setStreamType(StreamType::UncompressedVideo);
    vsi->setColorFormat(ColorFormat::YUV420);
    vsi->setScanType(ScanType::Progressive);

    auto pin = primo::make_ref(Library::createMediaPin());
    pin->setStreamInfo(vsi.get());
    
    auto socket = primo::make_ref(Library::createMediaSocket());
    socket->setStreamType(StreamType::UncompressedVideo);
    socket->pins()->add(pin.get());
    
    return socket;
}

bool decodeAvcPull(Options& opt)
{
    // transcoder will fail if output exists (by design)
    deleteFile(primo::ustring(opt.outputFile));

    ofstream outfile(opt.outputFile, ios_base::binary);
    if (!outfile.is_open())
    {
        cout << "Could not open file " << opt.outputFile << endl;
        return false;
    }

    // create input socket from file
    auto inSocket = primo::make_ref(Library::createMediaSocket());
    inSocket->setFile(primo::ustring(opt.inputFile));

    // create output socket with one YUV 4:2:0 video pin
    auto outSocket = createOutputSocket(opt);

    // create transcoder
    auto transcoder = primo::make_ref(Library::createTranscoder());
    transcoder->setAllowDemoMode(true);
    transcoder->inputs()->add(inSocket.get());
    transcoder->outputs()->add(outSocket.get());

    if (!transcoder->open())
    {
        printError("Transcoder::open", transcoder->error());
        return false;
    }

    // decode by pulling decoded samples
    int32_t inputIndex = 0;
    auto yuvFrame = primo::make_ref(Library::createMediaSample());
    int32_t frameIndex = 0;

    while (transcoder->pull(inputIndex, yuvFrame.get()))
    {
        // Each call to Transcoder::pull returns a raw YUV 4:2:0 frame
        outfile.write((const char *)yuvFrame->buffer()->data(), yuvFrame->buffer()->dataSize());
        
        cout << "Frame Index: " << frameIndex << endl;
        frameIndex++;
    }

    const primo::error::ErrorInfo* error = transcoder->error();
    printStatus(error);

    bool success = false;
    if ((error->facility() == primo::error::ErrorFacility::Codec) &&
        (error->code() == primo::codecs::CodecError::EOS))
    {
        // ok - end of stream
        success = true;
    }

    transcoder->close();

    cout << "Output: " << opt.outputFile << endl;
    cout << "Total frames: " << frameIndex << endl;

    return success;
}

int main(int argc, char* argv[])
{
    Options opt;
    
    switch(prepareOptions(opt, argc, argv))
    {
        case Command: return 0;
        case Error:	return 1;
        case Parsed: break;
    }
    
    Library::initialize();
    
    bool decodeResult = decodeAvcPull(opt);
    
    Library::shutdown();
    
    return decodeResult ? 0 : 1;
}
#include <unistd.h>

#include <iostream>
#include <string>
#include <fstream>
#include <filesystem>

#include <primo/platform/reference++.h>
#include <primo/platform/error_facility.h>
#include <primo/platform/ustring.h>

#include <primo/avblocks/avb.h>

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

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

namespace fs = std::filesystem;
namespace av = primo::avblocks;
namespace pc = primo::codecs;

void printStatus(const primo::error::ErrorInfo* e)
{
    if (primo::error::ErrorFacility::Success == e->facility())
    {
        cout << "Success";
        
    }
    else
    {
        if (e->message())
        {
            cout << primo::ustring(e->message()) << " ";
        }
        
        cout << "(facility:" << e->facility() << " error:" << e->code() << ")" << endl;
    }
    
    cout << endl;
}

primo::ref<MediaSocket> createOutputSocket(Options& opt)
{
    auto vsi = primo::make_ref(Library::createVideoStreamInfo());
    vsi->setStreamType(StreamType::UncompressedVideo);
    vsi->setColorFormat(ColorFormat::YUV420);
    vsi->setScanType(ScanType::Progressive);

    auto pin = primo::make_ref(Library::createMediaPin());
    pin->setStreamInfo(vsi.get());
    
    auto socket = primo::make_ref(Library::createMediaSocket());
    socket->setStreamType(StreamType::UncompressedVideo);
    socket->pins()->add(pin.get());
    
    return socket;
}

bool decodeAvcPull(Options& opt)
{
    // transcoder will fail if output exists (by design)
    deleteFile(primo::ustring(opt.outputFile));

    ofstream outfile(opt.outputFile, ios_base::binary);
    if (!outfile.is_open())
    {
        cout << "Could not open file " << opt.outputFile << endl;
        return false;
    }

    // create input socket from file
    auto inSocket = primo::make_ref(Library::createMediaSocket());
    inSocket->setFile(primo::ustring(opt.inputFile));

    // create output socket with one YUV 4:2:0 video pin
    auto outSocket = createOutputSocket(opt);

    // create transcoder
    auto transcoder = primo::make_ref(Library::createTranscoder());
    transcoder->setAllowDemoMode(true);
    transcoder->inputs()->add(inSocket.get());
    transcoder->outputs()->add(outSocket.get());

    if (!transcoder->open())
    {
        printError("Transcoder::open", transcoder->error());
        return false;
    }

    // decode by pulling decoded samples
    int32_t inputIndex = 0;
    auto yuvFrame = primo::make_ref(Library::createMediaSample());
    int32_t frameIndex = 0;

    while (transcoder->pull(inputIndex, yuvFrame.get()))
    {
        // Each call to Transcoder::pull returns a raw YUV 4:2:0 frame
        outfile.write((const char *)yuvFrame->buffer()->data(), yuvFrame->buffer()->dataSize());
        
        cout << "Frame Index: " << frameIndex << endl;
        frameIndex++;
    }

    const primo::error::ErrorInfo* error = transcoder->error();
    printStatus(error);

    bool success = false;
    if ((error->facility() == primo::error::ErrorFacility::Codec) &&
        (error->code() == primo::codecs::CodecError::EOS))
    {
        // ok - end of stream
        success = true;
    }

    transcoder->close();

    cout << "Output: " << opt.outputFile << endl;
    cout << "Total frames: " << frameIndex << endl;

    return success;
}

int main(int argc, char* argv[])
{
    Options opt;
    
    switch(prepareOptions(opt, argc, argv))
    {
        case Command: return 0;
        case Error:	return 1;
        case Parsed: break;
    }
    
    Library::initialize();
    
    bool decodeResult = decodeAvcPull(opt);
    
    Library::shutdown();
    
    return decodeResult ? 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