AVC / H.264 Annex B Encoder (Push)

This article explains how you can use TTranscoder::push to encode an AVC / H.264 elementary stream from raw YUV frames.

The code snippets in this article are from the enc_avc_push macOS sample.

Linux and Windows samples are also available:

Source Video

For source we use the foreman_qcif.yuv file from the AVBlocks Assets repository. After downloading and unzipping you will find foreman_qcif.yuv in the vid subdirectory.

Code

This code takes a raw YUV video file and encodes it to an H.264 stream using the push method. Two TTranscoder objects are used: one to read raw YUV frames from the file, and another to encode those frames to AVC / H.264 Annex B.

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 encode(opt) ? 0 : 1;
}

Configure YUV Video Stream Info

This helper function creates a TVideoStreamInfo object describing uncompressed YUV video. The frame dimensions, frame rate, color format, and scan type are all configured here. This same stream info is used for both the YUV reader output and the H.264 encoder input.

static TVideoStreamInfo createYuvVideoInfo(const Options& opt)
{
    return TVideoStreamInfo()
        .streamType(StreamType::UncompressedVideo)
        .frameRate(opt.fps)
        .frameWidth(opt.frame_size.width_)
        .frameHeight(opt.frame_size.height_)
        .colorFormat(opt.yuv_color.Id)
        .scanType(ScanType::Progressive);
}

Configure YUV Reader Transcoder

The YUV reader transcoder reads the source YUV file and outputs raw uncompressed frames. It creates an input socket pointing to the YUV file and an output socket configured for uncompressed video. The transcoder is opened to prepare for reading frames one by one using the pull method.

TTranscoder yuvReader;
yuvReader
    .allowDemoMode(true)
    .addInput(
        TMediaSocket()
            .file(opt.yuv_file)
            .streamType(StreamType::UncompressedVideo)
            .addPin(TMediaPin().streamInfo(createYuvVideoInfo(opt)))
    )
    .addOutput(
        TMediaSocket()
            .streamType(StreamType::UncompressedVideo)
            .addPin(TMediaPin().streamInfo(createYuvVideoInfo(opt)))
    )
    .open();

Configure H.264 Encoder Transcoder

The H.264 encoder transcoder takes raw YUV frames and encodes them to H.264 Annex B format. It creates an input socket for uncompressed YUV video and an output socket configured for H.264. The output file path is set on the encoder socket.

Hardware acceleration can be enabled or disabled by setting the Param::HardwareEncoder parameter on the output pin.

TTranscoder encoder;
encoder
    .allowDemoMode(true)
    .addInput(
        TMediaSocket()
            .streamType(StreamType::UncompressedVideo)
            .addPin(TMediaPin().streamInfo(createYuvVideoInfo(opt)))
    )
    .addOutput(
        // default subtype for StreamType::H264 is StreamSubType::AVC_Annex_B
        TMediaSocket()
            .file(opt.h264_file)
            .streamType(StreamType::H264)
            .addPin(
                TMediaPin()
                    .streamInfo(TVideoStreamInfo()
                        .streamType(StreamType::H264)
                        .frameRate(opt.fps)
                        .frameWidth(opt.frame_size.width_)
                        .frameHeight(opt.frame_size.height_)
                        .colorFormat(opt.yuv_color.Id)
                        .scanType(ScanType::Progressive)
                    )
                    // enable or disable hardware acceleration
                    .addParam(Param::HardwareEncoder, HardwareEncoder::Auto)
            )
    )
    .open();

Encode with Push

This is the main encoding function. It opens both the YUV reader and H.264 encoder transcoders, then runs a push loop that pulls YUV frames from the reader and pushes them to the encoder. After all frames are processed, the encoder is flushed to finalize the output.

The function checks error codes at each step to determine if encoding completed successfully or if an error occurred.

int32_t yuvOutputIndex = 0;
TMediaSample yuvFrame;
bool success = false;

for (;;)
{
    if (yuvReader.pull(yuvOutputIndex, yuvFrame))
    {
        if (!encoder.push(0, yuvFrame))
        {
            printError("H264 encoder push", encoder.error());
            break;
        }
        continue;
    }

    const auto error = yuvReader.error();
    if (error.facility() != primo::error::ErrorFacility::Codec ||
        error.code() != CodecError::EOS)
    {
        printError("YUV reader pull", error);
        break;
    }

    // end of input - flush the encoder
    if (!encoder.flush())
    {
        printError("H264 encoder flush", encoder.error());
        break;
    }

    success = true;
    break;
}

yuvReader.close();
encoder.close();

Run Encoding Pipeline

This function orchestrates the entire encoding pipeline. It deletes the output file if it exists (the transcoder will fail if the output already exists), creates and opens both transcoders, runs the encoding loop, and properly closes everything.

bool encode(const Options& opt)
{
    // transcoder will fail if output exists (by design)
    deleteFile(opt.h264_file.c_str());

    try {
        // YUV reader: simulates a raw video source (e.g. a capture device).
        // The output socket has no file, so frames are pulled one by one.
        TTranscoder yuvReader;
        yuvReader
            .allowDemoMode(true)
            .addInput(
                TMediaSocket()
                    .file(opt.yuv_file)
                    .streamType(StreamType::UncompressedVideo)
                    .addPin(TMediaPin().streamInfo(createYuvVideoInfo(opt)))
            )
            .addOutput(
                TMediaSocket()
                    .streamType(StreamType::UncompressedVideo)
                    .addPin(TMediaPin().streamInfo(createYuvVideoInfo(opt)))
            )
            .open();

        // H.264 encoder: receives pushed YUV frames, writes AVC/H.264 Annex B to file.
        TTranscoder encoder;
        encoder
            .allowDemoMode(true)
            .addInput(
                TMediaSocket()
                    .streamType(StreamType::UncompressedVideo)
                    .addPin(TMediaPin().streamInfo(createYuvVideoInfo(opt)))
            )
            .addOutput(
                // default subtype for StreamType::H264 is StreamSubType::AVC_Annex_B
                TMediaSocket()
                    .file(opt.h264_file)
                    .streamType(StreamType::H264)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TVideoStreamInfo()
                                .streamType(StreamType::H264)
                                .frameRate(opt.fps)
                                .frameWidth(opt.frame_size.width_)
                                .frameHeight(opt.frame_size.height_)
                                .colorFormat(opt.yuv_color.Id)
                                .scanType(ScanType::Progressive)
                            )
                            // enable or disable hardware acceleration
                            .addParam(Param::HardwareEncoder, HardwareEncoder::Auto)
                    )
            )
            .open();

        // Push loop: pull a raw frame from yuvReader, push it to the encoder
        int32_t yuvOutputIndex = 0;
        TMediaSample yuvFrame;
        bool success = false;

        for (;;)
        {
            if (yuvReader.pull(yuvOutputIndex, yuvFrame))
            {
                if (!encoder.push(0, yuvFrame))
                {
                    printError("H264 encoder push", encoder.error());
                    break;
                }
                continue;
            }

            const auto error = yuvReader.error();
            if (error.facility() != primo::error::ErrorFacility::Codec ||
                error.code() != CodecError::EOS)
            {
                printError("YUV reader pull", error);
                break;
            }

            // end of input - flush the encoder
            if (!encoder.flush())
            {
                printError("H264 encoder flush", encoder.error());
                break;
            }

            success = true;
            break;
        }

        yuvReader.close();
        encoder.close();

        if (success)
            std::println("Output: {}", opt.h264_file);

        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;
    }
}

Complete C++ Code

Here’s the complete working example that demonstrates H.264 encoding 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 encoding operation using push, and properly shuts down the library before exiting.

#include <primo/avblocks/avb++.h>

#include <print>

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

using namespace primo::codecs;
using namespace primo::avblocks;
using namespace primo::avblocks::modern;

static TVideoStreamInfo createYuvVideoInfo(const Options& opt)
{
    return TVideoStreamInfo()
        .streamType(StreamType::UncompressedVideo)
        .frameRate(opt.fps)
        .frameWidth(opt.frame_size.width_)
        .frameHeight(opt.frame_size.height_)
        .colorFormat(opt.yuv_color.Id)
        .scanType(ScanType::Progressive);
}

bool encode(const Options& opt)
{
    // transcoder will fail if output exists (by design)
    deleteFile(opt.h264_file.c_str());

    try {
        // YUV reader: simulates a raw video source (e.g. a capture device).
        // The output socket has no file, so frames are pulled one by one.
        TTranscoder yuvReader;
        yuvReader
            .allowDemoMode(true)
            .addInput(
                TMediaSocket()
                    .file(opt.yuv_file)
                    .streamType(StreamType::UncompressedVideo)
                    .addPin(TMediaPin().streamInfo(createYuvVideoInfo(opt)))
            )
            .addOutput(
                TMediaSocket()
                    .streamType(StreamType::UncompressedVideo)
                    .addPin(TMediaPin().streamInfo(createYuvVideoInfo(opt)))
            )
            .open();

        // H.264 encoder: receives pushed YUV frames, writes AVC/H.264 Annex B to file.
        TTranscoder encoder;
        encoder
            .allowDemoMode(true)
            .addInput(
                TMediaSocket()
                    .streamType(StreamType::UncompressedVideo)
                    .addPin(TMediaPin().streamInfo(createYuvVideoInfo(opt)))
            )
            .addOutput(
                // default subtype for StreamType::H264 is StreamSubType::AVC_Annex_B
                TMediaSocket()
                    .file(opt.h264_file)
                    .streamType(StreamType::H264)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TVideoStreamInfo()
                                .streamType(StreamType::H264)
                                .frameRate(opt.fps)
                                .frameWidth(opt.frame_size.width_)
                                .frameHeight(opt.frame_size.height_)
                                .colorFormat(opt.yuv_color.Id)
                                .scanType(ScanType::Progressive)
                            )
                            // enable or disable hardware acceleration
                            .addParam(Param::HardwareEncoder, HardwareEncoder::Auto)
                    )
            )
            .open();

        // Push loop: pull a raw frame from yuvReader, push it to the encoder
        int32_t yuvOutputIndex = 0;
        TMediaSample yuvFrame;
        bool success = false;

        for (;;)
        {
            if (yuvReader.pull(yuvOutputIndex, yuvFrame))
            {
                if (!encoder.push(0, yuvFrame))
                {
                    printError("H264 encoder push", encoder.error());
                    break;
                }
                continue;
            }

            const auto error = yuvReader.error();
            if (error.facility() != primo::error::ErrorFacility::Codec ||
                error.code() != CodecError::EOS)
            {
                printError("YUV reader pull", error);
                break;
            }

            // end of input - flush the encoder
            if (!encoder.flush())
            {
                printError("H264 encoder flush", encoder.error());
                break;
            }

            success = true;
            break;
        }

        yuvReader.close();
        encoder.close();

        if (success)
            std::println("Output: {}", opt.h264_file);

        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 encode(opt) ? 0 : 1;
}

How to Run

See the build instructions for macOS and the enc_avc_push example for details.

Command Line

./enc_avc_push --frame <width>x<height> --rate <fps> --color <COLOR> --input <file.yuv> --output <file.h264> [--colors]	[--help]

Examples

List options:

./bin/x64/enc_avc_push --help
enc_avc_push --frame <width>x<height> --rate <fps> --color <COLOR> --input <file.yuv> --output <file.h264> [--colors]
  -h,    --help
  -i,    --input    input YUV file
  -o,    --output   output H264 file
  -r,    --rate     input frame rate
  -f,    --frame    input frame sizes <width>x<height>
  -c,    --color    input color format. Use --colors to list all supported color
                    formats
         --colors   list COLOR constants

Encode a raw YUV video from ./assets/vid/foreman_qcif.yuv to a H.264 video in ./output/enc_avc_push/foreman_qcif.h264:

mkdir -p ./output/enc_avc_push

./bin/x64/enc_avc_push \
  --input ./assets/vid/foreman_qcif.yuv \
  --output ./output/enc_avc_push/foreman_qcif.h264 \
  --frame 176x144 \
  --rate 30 \
  --color yuv420