AVC / H.264 Encoder (Pull)

This article explains how to use TTranscoder::pull to encode a raw YUV video file to an AVC / H.264 Annex B elementary stream.

The code snippets in this article are from the enc_avc_pull 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 AVC / H.264 Annex B format using the pull method. The encoded samples are pulled from the transcoder and written to the output file by the application.

Initialize AVBlocks

Initialize the AVBlocks library before creating the transcoder, and shut it down after encoding is complete.

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 Input Socket

The input socket describes the raw YUV source file. The frame size, frame rate, and color format must match the source video.

TMediaSocket()
    .file(opt.yuv_file)
    .streamType(StreamType::UncompressedVideo)
    .addPin(
        TMediaPin()
            .streamInfo(TVideoStreamInfo()
                .streamType(StreamType::UncompressedVideo)
                .frameWidth(opt.frame_size.width_)
                .frameHeight(opt.frame_size.height_)
                .colorFormat(opt.yuv_color.Id)
                .frameRate(opt.fps)
                .scanType(ScanType::Progressive)
            )
    )

Configure Output Socket

The output socket describes AVC / H.264 Annex B output. The pull sample does not set a file on the output socket because the application writes pulled samples to the output stream.

TMediaSocket()
    .streamType(StreamType::H264)
    .streamSubType(StreamSubType::AVC_Annex_B)
    .addPin(
        TMediaPin()
            .streamInfo(TVideoStreamInfo()
                .streamType(StreamType::H264)
                .streamSubType(StreamSubType::AVC_Annex_B)
            )
    )

Configure Transcoder and Pull Encoded Samples

After creating the input and output sockets, the sample creates a transcoder, enables demo mode, opens it, pulls encoded samples, writes each sample buffer to the output file, and checks for end-of-stream.

bool encode(const Options& opt)
{
    deleteFile(opt.h264_file.c_str());

    std::ofstream outfile(opt.h264_file, std::ios_base::binary);
    if (!outfile.is_open())
    {
        std::println(stderr, "Could not open output file: {}", opt.h264_file);
        return false;
    }

    try {
        TTranscoder transcoder;
        transcoder
            .allowDemoMode(true)
            .addInput(
                TMediaSocket()
                    .file(opt.yuv_file)
                    .streamType(StreamType::UncompressedVideo)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TVideoStreamInfo()
                                .streamType(StreamType::UncompressedVideo)
                                .frameWidth(opt.frame_size.width_)
                                .frameHeight(opt.frame_size.height_)
                                .colorFormat(opt.yuv_color.Id)
                                .frameRate(opt.fps)
                                .scanType(ScanType::Progressive)
                            )
                    )
            )
            .addOutput(
                TMediaSocket()
                    .streamType(StreamType::H264)
                    .streamSubType(StreamSubType::AVC_Annex_B)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TVideoStreamInfo()
                                .streamType(StreamType::H264)
                                .streamSubType(StreamSubType::AVC_Annex_B)
                            )
                    )
            )
            .open();

        int32_t outputIndex = 0;
        TMediaSample sample;

        while (transcoder.pull(outputIndex, sample))
        {
            auto buf = sample.buffer();
            outfile.write(reinterpret_cast<const char*>(buf.data()), buf.dataSize());
        }

        const auto error = transcoder.error();
        bool success = error.facility() == primo::error::ErrorFacility::Codec &&
                       error.code()     == primo::codecs::CodecError::EOS;

        if (!success)
        {
            printError("Transcoder pull", error);
            transcoder.close();
            return false;
        }

        transcoder.close();

        std::println("Output: {}", opt.h264_file);
        return true;

    } 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 from enc_avc_pull.cpp:

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

#include <print>
#include <fstream>

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

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

bool encode(const Options& opt)
{
    deleteFile(opt.h264_file.c_str());

    std::ofstream outfile(opt.h264_file, std::ios_base::binary);
    if (!outfile.is_open())
    {
        std::println(stderr, "Could not open output file: {}", opt.h264_file);
        return false;
    }

    try {
        TTranscoder transcoder;
        transcoder
            .allowDemoMode(true)
            .addInput(
                TMediaSocket()
                    .file(opt.yuv_file)
                    .streamType(StreamType::UncompressedVideo)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TVideoStreamInfo()
                                .streamType(StreamType::UncompressedVideo)
                                .frameWidth(opt.frame_size.width_)
                                .frameHeight(opt.frame_size.height_)
                                .colorFormat(opt.yuv_color.Id)
                                .frameRate(opt.fps)
                                .scanType(ScanType::Progressive)
                            )
                    )
            )
            .addOutput(
                TMediaSocket()
                    .streamType(StreamType::H264)
                    .streamSubType(StreamSubType::AVC_Annex_B)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TVideoStreamInfo()
                                .streamType(StreamType::H264)
                                .streamSubType(StreamSubType::AVC_Annex_B)
                            )
                    )
            )
            .open();

        int32_t outputIndex = 0;
        TMediaSample sample;

        while (transcoder.pull(outputIndex, sample))
        {
            auto buf = sample.buffer();
            outfile.write(reinterpret_cast<const char*>(buf.data()), buf.dataSize());
        }

        const auto error = transcoder.error();
        bool success = error.facility() == primo::error::ErrorFacility::Codec &&
                       error.code()     == primo::codecs::CodecError::EOS;

        if (!success)
        {
            printError("Transcoder pull", error);
            transcoder.close();
            return false;
        }

        transcoder.close();

        std::println("Output: {}", opt.h264_file);
        return true;

    } 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_pull example for details.

Command Line

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

Examples

List options:

./bin/x64/enc_avc_pull --help
enc_avc_pull --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 an H.264 video in ./output/enc_avc_pull/foreman_qcif.h264:

mkdir -p ./output/enc_avc_pull

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