AVC / H.264 Annex B Encoder (Push)

This article explains how you can use Transcoder::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 Transcoder 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 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 result = encode_h264_stream(opt);
    Library::shutdown();

    return result ? 0 : 1;
}

Configure YUV Video Stream Info

This helper function creates a VideoStreamInfo 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.

primo::ref<VideoStreamInfo> create_yuv_video_info(Options& opt)
{
    primo::ref<VideoStreamInfo> yuvVideo(Library::createVideoStreamInfo());

    yuvVideo->setStreamType(StreamType::UncompressedVideo);
    yuvVideo->setFrameRate(opt.fps);
    yuvVideo->setFrameWidth(opt.frame_size.width_);
    yuvVideo->setFrameHeight(opt.frame_size.height_);
    yuvVideo->setColorFormat(opt.yuv_color.Id);
    yuvVideo->setScanType(ScanType::Progressive);

    return yuvVideo;
}

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.

primo::ref<Transcoder> create_yuv_reader(Options& opt)
{
    // Create VideoStreamInfo, MediaPin, and MediaSocket describing the YUV input.
    primo::ref<VideoStreamInfo> yuvInVideo = create_yuv_video_info(opt);

    primo::ref<MediaPin> yuvInPin(Library::createMediaPin());
    yuvInPin->setStreamInfo(yuvInVideo.get());

    primo::ref<MediaSocket> yuvInSocket(Library::createMediaSocket());
    yuvInSocket->setStreamType(StreamType::UncompressedVideo);
    yuvInSocket->setFile(primo::ustring(opt.yuv_file));

    yuvInSocket->pins()->add(yuvInPin.get());

    // Create VideoStreamInfo, MediaPin, and MediaSocket describing the YUV output.
    // This is the same as the input, but no output file is set on the MediaSocket,
    // because we want to pull frames one by one using Transcoder::pull.
    primo::ref<VideoStreamInfo> yuvOutVideo = create_yuv_video_info(opt);

    primo::ref<MediaPin> yuvOutPin(Library::createMediaPin());
    yuvOutPin->setStreamInfo(yuvOutVideo.get());

    primo::ref<MediaSocket> yuvOutSocket(Library::createMediaSocket());
    yuvOutSocket->setStreamType(StreamType::UncompressedVideo);

    yuvOutSocket->pins()->add(yuvOutPin.get());

    // Create Transcoder.
    primo::ref<Transcoder> yuvReader(Library::createTranscoder());
    yuvReader->setAllowDemoMode(true);
    yuvReader->inputs()->add(yuvInSocket.get());
    yuvReader->outputs()->add(yuvOutSocket.get());

    return yuvReader;
}

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 before creating the transcoder.

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

primo::ref<Transcoder> create_h264_encoder(Options& opt, HardwareEncoder::Enum hardware)
{
    // Create VideoStreamInfo, MediaPin, and MediaSocket describing the YUV input.
    primo::ref<VideoStreamInfo> yuvInVideo = create_yuv_video_info(opt);

    primo::ref<MediaPin> yuvInPin(Library::createMediaPin());
    yuvInPin->setStreamInfo(yuvInVideo.get());

    primo::ref<MediaSocket> yuvInSocket(Library::createMediaSocket());
    yuvInSocket->setStreamType(StreamType::UncompressedVideo);

    yuvInSocket->pins()->add(yuvInPin.get());

    // Create VideoStreamInfo, MediaPin, and MediaSocket describing the H.264 output.
    primo::ref<VideoStreamInfo> h264OutVideo(Library::createVideoStreamInfo());
    h264OutVideo->setStreamType(StreamType::H264);
    h264OutVideo->setFrameRate(opt.fps);
    h264OutVideo->setFrameWidth(opt.frame_size.width_);
    h264OutVideo->setFrameHeight(opt.frame_size.height_);
    h264OutVideo->setColorFormat(opt.yuv_color.Id);
    h264OutVideo->setScanType(ScanType::Progressive);

    primo::ref<MediaPin> h264OutPin(Library::createMediaPin());
    h264OutPin->setStreamInfo(h264OutVideo.get());

    // Enable or disable hardware acceleration.
    h264OutPin->params()->addInt(Param::HardwareEncoder, hardware);

    primo::ref<MediaSocket> h264OutSocket(Library::createMediaSocket());
    h264OutSocket->setStreamType(StreamType::H264);
    // Default subtype for StreamType::H264 is StreamSubType::AVC_Annex_B.
    h264OutSocket->setFile(primo::ustring(opt.h264_file));

    h264OutSocket->pins()->add(h264OutPin.get());

    // Transcoder.
    primo::ref<Transcoder> h264Encoder(Library::createTranscoder());
    h264Encoder->setAllowDemoMode(true);
    h264Encoder->inputs()->add(yuvInSocket.get());
    h264Encoder->outputs()->add(h264OutSocket.get());

    return h264Encoder;
}

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.

bool encode_h264_stream(Transcoder* yuvReader, Transcoder* h264Encoder)
{
    int32_t inputIndex = 0;
    primo::ref<MediaSample> yuvFrame(Library::createMediaSample());

    while (true)
    {
        // Simulate a raw frame. Each call to Transcoder::pull returns one video frame.
        if (!yuvReader->pull(inputIndex, yuvFrame.get()))
            break;

        // Pass the raw video frame to Transcoder::push to encode it as AVC / H.264.
        if (!h264Encoder->push(0, yuvFrame.get()))
        {
            printError("H264 encoder push", h264Encoder->error());
            return false;
        }
    }

    const primo::error::ErrorInfo* error = yuvReader->error();
    if (error->facility() != primo::error::ErrorFacility::Codec ||
        error->code() != CodecError::EOS)
    {
        printError("YUV reader pull", error);
        return false;
    }

    if (!h264Encoder->flush())
    {
        printError("H264 encoder flush", h264Encoder->error());
        return false;
    }

    return true;
}

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_h264_stream(Options& opt)
{
    // Transcoder will fail if the output file already exists.
    deleteFile(primo::ustring(opt.h264_file));

    // Create a reader to simulate raw video frames. In reality, the raw video source
    // could be different, for example some kind of video capture device.
    primo::ref<Transcoder> yuvReader = create_yuv_reader(opt);

    // Create a H.264 encoder. We will pass the raw video frames to it to encode them as H.264.
    primo::ref<Transcoder> h264Encoder = create_h264_encoder(opt, HardwareEncoder::Auto);

    if (!yuvReader->open())
    {
        printError("YUV reader open", yuvReader->error());
        return false;
    }

    bool success = false;
    if (h264Encoder->open())
    {
        success = encode_h264_stream(yuvReader.get(), h264Encoder.get());
        h264Encoder->close();
    }
    else
    {
        printError("H264 encoder open", h264Encoder->error());
    }

    yuvReader->close();

    return success;
}

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.

/*
 *  Copyright (c)  Primo Software. All Rights Reserved.
 *
 *  Use of this source code is governed by a MIT License
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */

#include <primo/avblocks/avb.h>

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

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

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

primo::ref<VideoStreamInfo> create_yuv_video_info(Options& opt)
{
    primo::ref<VideoStreamInfo> yuvVideo(Library::createVideoStreamInfo());

    yuvVideo->setStreamType(StreamType::UncompressedVideo);
    yuvVideo->setFrameRate(opt.fps);
    yuvVideo->setFrameWidth(opt.frame_size.width_);
    yuvVideo->setFrameHeight(opt.frame_size.height_);
    yuvVideo->setColorFormat(opt.yuv_color.Id);
    yuvVideo->setScanType(ScanType::Progressive);

    return yuvVideo;
}

primo::ref<Transcoder> create_yuv_reader(Options& opt)
{
    // Create VideoStreamInfo, MediaPin, and MediaSocket describing the YUV input.
    primo::ref<VideoStreamInfo> yuvInVideo = create_yuv_video_info(opt);

    primo::ref<MediaPin> yuvInPin(Library::createMediaPin());
    yuvInPin->setStreamInfo(yuvInVideo.get());

    primo::ref<MediaSocket> yuvInSocket(Library::createMediaSocket());
    yuvInSocket->setStreamType(StreamType::UncompressedVideo);
    yuvInSocket->setFile(primo::ustring(opt.yuv_file));

    yuvInSocket->pins()->add(yuvInPin.get());

    // Create VideoStreamInfo, MediaPin, and MediaSocket describing the YUV output.
    // This is the same as the input, but no output file is set on the MediaSocket,
    // because we want to pull frames one by one using Transcoder::pull.
    primo::ref<VideoStreamInfo> yuvOutVideo = create_yuv_video_info(opt);

    primo::ref<MediaPin> yuvOutPin(Library::createMediaPin());
    yuvOutPin->setStreamInfo(yuvOutVideo.get());

    primo::ref<MediaSocket> yuvOutSocket(Library::createMediaSocket());
    yuvOutSocket->setStreamType(StreamType::UncompressedVideo);

    yuvOutSocket->pins()->add(yuvOutPin.get());

    // Create Transcoder.
    primo::ref<Transcoder> yuvReader(Library::createTranscoder());
    yuvReader->setAllowDemoMode(true);
    yuvReader->inputs()->add(yuvInSocket.get());
    yuvReader->outputs()->add(yuvOutSocket.get());

    return yuvReader;
}

primo::ref<Transcoder> create_h264_encoder(Options& opt, HardwareEncoder::Enum hardware)
{
    // Create VideoStreamInfo, MediaPin, and MediaSocket describing the YUV input.
    primo::ref<VideoStreamInfo> yuvInVideo = create_yuv_video_info(opt);

    primo::ref<MediaPin> yuvInPin(Library::createMediaPin());
    yuvInPin->setStreamInfo(yuvInVideo.get());

    primo::ref<MediaSocket> yuvInSocket(Library::createMediaSocket());
    yuvInSocket->setStreamType(StreamType::UncompressedVideo);

    yuvInSocket->pins()->add(yuvInPin.get());

    // Create VideoStreamInfo, MediaPin, and MediaSocket describing the H.264 output.
    primo::ref<VideoStreamInfo> h264OutVideo(Library::createVideoStreamInfo());
    h264OutVideo->setStreamType(StreamType::H264);
    h264OutVideo->setFrameRate(opt.fps);
    h264OutVideo->setFrameWidth(opt.frame_size.width_);
    h264OutVideo->setFrameHeight(opt.frame_size.height_);
    h264OutVideo->setColorFormat(opt.yuv_color.Id);
    h264OutVideo->setScanType(ScanType::Progressive);

    primo::ref<MediaPin> h264OutPin(Library::createMediaPin());
    h264OutPin->setStreamInfo(h264OutVideo.get());

    // Enable or disable hardware acceleration.
    h264OutPin->params()->addInt(Param::HardwareEncoder, hardware);

    primo::ref<MediaSocket> h264OutSocket(Library::createMediaSocket());
    h264OutSocket->setStreamType(StreamType::H264);
    // Default subtype for StreamType::H264 is StreamSubType::AVC_Annex_B.
    h264OutSocket->setFile(primo::ustring(opt.h264_file));

    h264OutSocket->pins()->add(h264OutPin.get());

    // Transcoder.
    primo::ref<Transcoder> h264Encoder(Library::createTranscoder());
    h264Encoder->setAllowDemoMode(true);
    h264Encoder->inputs()->add(yuvInSocket.get());
    h264Encoder->outputs()->add(h264OutSocket.get());

    return h264Encoder;
}

bool encode_h264_stream(Transcoder* yuvReader, Transcoder* h264Encoder)
{
    int32_t inputIndex = 0;
    primo::ref<MediaSample> yuvFrame(Library::createMediaSample());

    while (true)
    {
        // Simulate a raw frame. Each call to Transcoder::pull returns one video frame.
        if (!yuvReader->pull(inputIndex, yuvFrame.get()))
            break;

        // Pass the raw video frame to Transcoder::push to encode it as AVC / H.264.
        if (!h264Encoder->push(0, yuvFrame.get()))
        {
            printError("H264 encoder push", h264Encoder->error());
            return false;
        }
    }

    const primo::error::ErrorInfo* error = yuvReader->error();
    if (error->facility() != primo::error::ErrorFacility::Codec ||
        error->code() != CodecError::EOS)
    {
        printError("YUV reader pull", error);
        return false;
    }

    if (!h264Encoder->flush())
    {
        printError("H264 encoder flush", h264Encoder->error());
        return false;
    }

    return true;
}

bool encode_h264_stream(Options& opt)
{
    // Transcoder will fail if the output file already exists.
    deleteFile(primo::ustring(opt.h264_file));

    // Create a reader to simulate raw video frames. In reality, the raw video source
    // could be different, for example some kind of video capture device.
    primo::ref<Transcoder> yuvReader = create_yuv_reader(opt);

    // Create a H.264 encoder. We will pass the raw video frames to it to encode them as H.264.
    primo::ref<Transcoder> h264Encoder = create_h264_encoder(opt, HardwareEncoder::Auto);

    if (!yuvReader->open())
    {
        printError("YUV reader open", yuvReader->error());
        return false;
    }

    bool success = false;
    if (h264Encoder->open())
    {
        success = encode_h264_stream(yuvReader.get(), h264Encoder.get());
        h264Encoder->close();
    }
    else
    {
        printError("H264 encoder open", h264Encoder->error());
    }

    yuvReader->close();

    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 result = encode_h264_stream(opt);
    Library::shutdown();

    return result ? 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