vencoderjpeg_web_logo

VEncoderJPEG C++ library

v1.0.1

Table of contents

Overview

The VEncoderJPEG C++ library provides a standalone software JPEG image and Motion-JPEG (MJPEG) video encoder for Windows, Linux and embedded platforms. The VEncoderJPEG class inherits the interface and data structures of the VCodec interface library and does not need a hardware encoder. The library depends only on the open-source VCodec library (provides codec interface, source code included, Apache 2.0 license) and uses the C++17 standard. Key features:

  • Baseline JPEG (ITU-T T.81, SOF0) with JFIF 1.02 header: 8-bit samples, standard Annex K Huffman tables, Annex K quantization tables scaled by the quality parameter with the scaling of RFC 2435, Appendix A (quality 100 gives steps of 1).
  • Input pixel formats: NV12, NV21, YU12 (I420), YV12, YUYV, UYVY, RGB24, BGR24, YUV24 and GRAY - every raw format of the Frame class.
  • Chroma subsampling: 4:2:0, 4:2:2, 4:4:4 and grayscale, selected automatically from the source format or set explicitly. Chroma is averaged or repeated when the source chroma resolution differs from the encoded one.
  • Multi-threading with identical output: a frame is split into bands of MCU rows that are transformed and Huffman-coded in parallel and then joined at bit level. The stream is byte-identical for any number of threads and contains no restart markers, so it can be decoded by any JPEG decoder.
  • Fast: 1920x1080 NV12 camera frame (quality 80) in 2.7 ms on one core, 0.75 ms with automatic threads (see Quality and performance). SSE2, SSSE3 and AVX2 kernels selected at run time from the CPU, so one binary runs on every x86-64 CPU; portable C++ kernels on other platforms; all kernels produce identical streams (see SIMD kernel levels).
  • Robust: invalid frames and parameters are rejected without memory access outside the source data; the output never exceeds the buffer size the Frame class allocates for compressed data; the methods throw no exceptions, memory or thread shortage is reported by the return value (see Security).
  • No sensitive data left behind: internal buffers holding frame data are overwritten with zeros by the RESET command and by the destructor.

Versions

Table 1 - Library versions.

Version Release date What’s new
1.0.0 30.09.2026 First version.
1.0.1 07.10.2026 SSE2, SSSE3 and AVX2 kernels selected at run time: the default build runs on every x86-64 CPU (the default build of 1.0.0 needed AVX2). Compiler warning and exploit mitigation options for all targets. Internal code reorganization. The encoded streams are unchanged.

Library files

The library is supplied as source code only. The user is provided with a set of files in the form of a CMake project (repository). The repository structure is shown below:

CMakeLists.txt ------------------ Main CMake file of the library.
README.md ----------------------- Documentation.
3rdparty ------------------------ Folder with third-party libraries.
    CMakeLists.txt -------------- CMake file to include third-party libraries.
    VCodec ---------------------- Folder with VCodec interface library files.
src ----------------------------- Folder with library source code.
    CMakeLists.txt -------------- CMake file of the library.
    VEncoderJPEG.h -------------- Main library header file (PIMPL).
    VEncoderJPEG.cpp ------------ Public class implementation.
    VEncoderJPEGVersion.h ------- Header file with library version.
    VEncoderJPEGVersion.h.in ---- File for CMake to generate version header.
    impl ------------------------ Internal implementation (namespace cr::video::jpegenc).
        VEncoderJPEGImpl.h/.cpp - Parameters, validation, destination frame management.
        FrameEncoder.h/.cpp ----- Encoder core: bands of MCU rows, threads, stream assembly.
        CodingSetup.h/.cpp ------ Geometry, tables and stream header kept between frames.
        SimdLevel.h/.cpp -------- Run-time selection of the kernel level (CPUID, XGETBV).
        BandCoder.h ------------- Interface of the kernel levels (bands, working memory).
        LevelConfig.h ----------- Instruction sets and namespace of the kernel level.
        LevelKernels.h ---------- Functions shared by the kernel sources of a level.
        BandCoder.cpp ----------- Kernel source: transform and coding of a band.
        StripFetch.cpp ---------- Kernel source: source formats to plane strips.
        ColorKernels.cpp -------- Kernel source: RGB to YCbCr conversion (JFIF).
        SpectrumKernels.h/.cpp -- Kernel source: forward DCT and quantization.
        TileCoder.h ------------- Kernel code: Huffman coding of quantized tiles.
        BitPacker.h ------------- Kernel code: bit accumulation of the Huffman coder.
        SourceAdapter.h/.cpp ---- Source pixel formats, conversion plan, range test.
        PictureLayout.h --------- Picture geometry (planes, MCU, tile order).
        BitSplicer.h/.cpp ------- Joining of bit sequences and 0xFF byte stuffing.
        StreamHeader.h/.cpp ----- JPEG marker segments (SOI, APP0, DQT, SOF0, DHT, SOS, EOI).
        QuantSteps.h/.cpp ------- Standard quantization tables, quality scaling.
        PrefixCodes.h/.cpp ------ Standard Huffman tables, canonical codes.
        WipedBuffer.h/.cpp ------ Byte buffer cleared before it is freed.
        WorkerPool.h/.cpp ------- Thread pool.
        BitOps.h ---------------- Portable bit and memory operations.
test ---------------------------- Folder for test applications.
    CMakeLists.txt -------------- CMake file for test application.
    main.cpp -------------------- Source C++ file of test application.
example ------------------------- Folder for simple example.
    CMakeLists.txt -------------- CMake file of example.
    main.cpp -------------------- Source C++ file of example.
benchmark ----------------------- Folder for benchmark application.
    CMakeLists.txt -------------- CMake file of benchmark.
    main.cpp -------------------- Source C++ file of benchmark.
static -------------------------- Folder with static files (logo).

VEncoderJPEG class description

VEncoderJPEG class declaration

The VEncoderJPEG class is declared in the VEncoderJPEG.h file. Class declaration:

namespace cr
{
namespace video
{
class VEncoderJPEG : public VCodec
{
public:

    /// Class constructor.
    VEncoderJPEG();

    /// Class destructor.
    ~VEncoderJPEG() override;

    /// Copy constructor (deleted).
    VEncoderJPEG(const VEncoderJPEG&) = delete;

    /// Move constructor (deleted).
    VEncoderJPEG(VEncoderJPEG&&) = delete;

    /// Copy assignment operator (deleted).
    VEncoderJPEG& operator=(const VEncoderJPEG&) = delete;

    /// Move assignment operator (deleted).
    VEncoderJPEG& operator=(VEncoderJPEG&&) = delete;

    /// Get library version.
    static std::string getVersion();

    /// Encode raw frame to JPEG.
    bool transcode(Frame& src, Frame& dst) override;

    /// Set parameter value.
    bool setParam(VCodecParam id, float value) override;

    /// Get parameter value.
    float getParam(VCodecParam id) override;

    /// Execute command.
    bool executeCommand(VCodecCommand id) override;
};
}
}

All methods are thread-safe: calls from different threads are serialized by an internal mutex. To encode several streams in parallel create one encoder object per stream. The encoder owns worker threads and buffers, so objects can be neither copied nor moved; use std::unique_ptr to pass an encoder around.

getVersion method

The getVersion() method returns a string of the current version of the VEncoderJPEG class. Method declaration:

static std::string getVersion();

This method can be used without a VEncoderJPEG class instance:

std::cout << "VEncoderJPEG class version: " << VEncoderJPEG::getVersion() << std::endl;

Console output:

VEncoderJPEG class version: 1.0.1

transcode method

The transcode(…) method encodes a raw video frame (Frame class) into a JPEG image. Every frame is encoded independently, so a sequence of frames forms a Motion-JPEG stream. Method declaration:

bool transcode(Frame& src, Frame& dst) override;
Parameter Value
src Source raw frame. Supported pixel formats (field fourcc): NV12, NV21, YU12, YV12, YUYV, UYVY, RGB24, BGR24, YUV24 and GRAY. Width and height: 1 - 65535 pixels; YUYV and UYVY need an even width, NV12, NV21, YU12 and YV12 need an even width and height. The field size must be at least the size of the pixel format (for example width * height * 3 / 2 for NV12).
dst Result frame. The encoder always produces Fourcc::JPEG data, so the frame does not need to be prepared. If dst has no data or its width, height or format differ from the source, the frame is reallocated as Frame(src.width, src.height, Fourcc::JPEG). After encoding dst.size holds the size of the JPEG image; frameId and sourceId are copied from the source.

Returns: TRUE if the frame was encoded or FALSE if not (unsupported format, invalid size or data size, src and dst being the same object, image too small for the JPEG headers, not enough memory). The method does not throw exceptions.

The Frame class allocates width * height * 4 bytes for compressed formats and copies of frames rely on that size, so the encoded image never exceeds it:

  • images smaller than about 13x13 pixels are refused, because the JPEG headers alone (about 620 bytes) do not fit;
  • a frame whose stream would exceed the limit is encoded again with reduced quality (a message is printed when logging is enabled). This happens only for extreme noise at quality above about 95 with 4:4:4 subsampling; camera images need less than a quarter of the limit.

setParam method

The setParam(…) method sets encoder parameters. Method declaration:

bool setParam(VCodecParam id, float value) override;
Parameter Description
id Parameter ID according to the VCodecParam enum.
value Parameter value. Valid values depend on the parameter ID.

Returns: TRUE if the parameter was set or FALSE if the parameter is not supported or the value is invalid.

The VCodec.h file of the VCodec library defines IDs of parameters (VCodecParam enum) and commands (VCodecCommand enum). New values are applied from the next frame.

Table 2 - Encoder parameters.

Parameter Access Description
LOG_LEVEL read / write Logging mode: 0 - disabled (default), 1 - console (messages to stderr), 2 - file (accepted, the library has no file output), 3 - console and file (console only). Messages describe refused frames and parameters.
QUALITY read / write JPEG quality 1 - 100, default 80. The value scales the Annex K quantization tables as described in RFC 2435, Appendix A; quality 100 gives steps of 1. 0 is accepted and treated as 1; values outside 0 - 100 are refused.
CUSTOM_1 read / write Chroma subsampling:
0 - automatic (default): 4:2:0 for NV12, NV21, YU12, YV12, RGB24, BGR24 and YUV24, 4:2:2 for YUYV and UYVY, grayscale for GRAY.
1 - 4:2:0.
2 - 4:2:2.
3 - 4:4:4.
4 - grayscale (luma only).
Chroma of higher resolution than encoded is averaged, chroma of lower resolution is repeated; a GRAY source encoded with colour gets neutral chroma.
CUSTOM_2 read / write Number of encoding threads:
1 - single thread (default).
0 - automatic: number of logical processors, at most 16.
N = 2 - 64 - N threads.
Small frames use fewer threads than configured (at least 300 MCUs per thread). The encoded stream does not depend on this parameter. If the system cannot create all threads, setParam(…) returns FALSE and the encoder continues with the threads that were started (their number is returned by getParam(…)).
CUSTOM_3 read / write Numeric range of YUV and GRAY sources (RGB sources are always full range):
0 - pass-through (default): samples are encoded as they are.
1 - automatic: frames that look like limited range video (no luma levels below 14 or above 238, luma reaching the limited range black or white level) are expanded to full range. Low-contrast full range content (fog, haze) is left unchanged.
2 - expansion of limited range: luma [16..235] and chroma [16..240] to [0..255].
JFIF images are full range: use 1 or 2 for sources in limited range (typical for video capture and decoders) to get correct contrast in image viewers.
TYPE read / write Not supported.
BITRATE_KBPS, MIN_BITRATE_KBPS, MAX_BITRATE_KBPS, BITRATE_MODE, FPS, GOP, H264_PROFILE read / write Not supported (JPEG has no rate control).

Example:

cr::video::VEncoderJPEG encoder{};
encoder.setParam(cr::video::VCodecParam::QUALITY, 90);   // Quality 90.
encoder.setParam(cr::video::VCodecParam::CUSTOM_1, 3);   // 4:4:4.
encoder.setParam(cr::video::VCodecParam::CUSTOM_2, 0);   // Automatic threads.

getParam method

The getParam(…) method returns encoder parameter values. Method declaration:

float getParam(VCodecParam id) override;
Parameter Description
id Parameter ID according to the VCodecParam enum (see Table 2).

Returns: parameter value or -1 if the parameter is not supported. QUALITY returns the applied value (1 - 100), CUSTOM_2 returns the value as set (0 for automatic) or the number of running threads if not all threads could be started.

executeCommand method

The executeCommand(…) method executes encoder commands. Method declaration:

bool executeCommand(VCodecCommand id) override;
Parameter Description
id Command ID according to the VCodecCommand enum.

Table 3 - Encoder commands.

Command Description
RESET Overwrites internal buffers (data of the last frame) with zeros, releases them and clears the cached state. Parameters are kept; the next frame is encoded normally.
MAKE_KEY_FRAME Accepted (returns TRUE) and has no effect: every JPEG frame is independently decodable.

Returns: TRUE if the command was executed or FALSE if not.

Output stream format

Every encoded frame is a complete JFIF file:

Segment Content
SOI Start of image.
APP0 JFIF 1.02, aspect ratio 1:1, no thumbnail.
DQT Luma table 0 and (for colour images) chroma table 1, 8-bit, zigzag order.
SOF0 Baseline DCT, 8-bit precision, components 1 (Y), 2 (Cb), 3 (Cr) with sampling factors 2x2/1x1/1x1 (4:2:0), 2x1/1x1/1x1 (4:2:2) or 1x1 (4:4:4 and grayscale).
DHT Standard Annex K.3 tables: DC/AC 0 for luma, DC/AC 1 for chroma.
SOS One interleaved scan with all components.
data Entropy-coded data without restart markers.
EOI End of image.

The standard Huffman tables keep the streams compatible with MJPEG consumers that assume them (for example RTP/JPEG, RFC 2435).

Security

Interfaces. The library is a component linked into the application. It has no network, file or inter-process interfaces, reads no environment variables or configuration files, loads no code at run time and needs no privileges. Its only inputs are the frames and parameters passed through the VCodec interface; its only outputs are the encoded frame and optional log messages on stderr (LOG_LEVEL 1 or 3, disabled by default). Messages contain frame sizes and parameter values, never frame data.

Input validation. Every frame is checked before any sample is read: pixel format, width and height (1 - 65535, even where the format requires it, width * height * 4 within the int range of the Frame class), data pointer and data size. Parameter values are range-checked (NaN and infinity are refused); a refused value leaves the configuration unchanged. The encoder relies on the Frame contract: src.data points to at least src.size readable bytes, and a dst frame of the source size and Fourcc::JPEG format owns width * height * 4 bytes, as allocated by the Frame class.

Memory safety. Buffer sizes are computed with overflow checks, and the stream is never larger than width * height * 4 bytes. The value ranges that the Huffman tables and the 16-bit and 32-bit vector arithmetic rely on are proven at compile time from the transform arithmetic (static_assert). Byte pointers are never reinterpreted as vector pointers.

Error handling. The methods throw no exceptions (only the constructor reports a failed allocation of the encoder object with std::bad_alloc). A failed memory allocation or thread creation makes transcode(…) or setParam(…) return FALSE; the encoder stays usable and keeps a consistent configuration.

Data handling. Frame data is processed in memory only; nothing is stored or transmitted. Internal buffers keep data of the last frame until the next frame is encoded. The RESET command and the destructor overwrite these buffers with zeros before releasing them. The destination frame and the source frame belong to the application.

Cryptography. The library contains no cryptographic algorithms, random number generators, keys or other security parameters. It is therefore outside the scope of FIPS 140-3 and does not interact with the cryptographic boundary of validated modules used in the same product. JPEG streams are not protected against disclosure or modification: apply the protection of the transport or storage where needed.

Components. The library consists of its own source code and the following components (git submodules; VCodec::getVersion() and Frame::getVersion() report the versions at run time):

Component Version Location Licence
VCodec 2.2.1 (commit e22888c) 3rdparty/VCodec Apache License 2.0
Frame 5.0.9 (commit da84b65) 3rdparty/VCodec/3rdparty/Frame Apache License 2.0

Products that distribute these components (in source or binary form) must include their licence texts (the LICENSE files of the submodules). Normative data: ITU-T T.81 (Annex K tables), JFIF 1.02, RFC 2435 (quality scaling).

Build hardening. The CMake project compiles every target of the repository (library, test application, example, benchmark) with warning and exploit mitigation options:

  • GCC and Clang: -Wall -Wextra -Wpedantic and further warnings, -fstack-protector-strong, -D_FORTIFY_SOURCE=2 (optimized builds), -fstack-clash-protection and -fcf-protection=full (x86) or -mbranch-protection=standard (ARM) where the compiler supports them; executables and shared libraries are linked with -z relro -z now -z noexecstack (Linux).
  • MSVC: /W4 /permissive- /sdl /GS /guard:cf; executables and shared libraries are linked with /DYNAMICBASE /NXCOMPAT /guard:cf.

The library is compiled in standard C++17 mode (no compiler extensions). Instruction set options are given only to the kernel sources of the vector levels (see SIMD kernel levels), never to the whole library.

Build and connect to your project

Prerequisites

  • C++17 compatible compiler (GCC 9+, Clang 10+, MSVC 2019+).
  • CMake 3.13 or higher.
  • No external libraries.

Build options

Option Default Description
VENCODERJPEG_SIMD ON Compile the vector kernels. x86: SSE2 base level and the SSSE3 and AVX2 levels, selected at run time (see SIMD kernel levels). OFF builds the portable C++ kernels only.
VENCODERJPEG_AVX2 ON Compile the AVX2 level on x86. OFF leaves it out, for example for compilers without AVX2 support; the binary then uses at most the SSSE3 level.

The encoded streams are identical for all options.

SIMD kernel levels

The kernels (source conversion, colour conversion, DCT and quantization, Huffman coding) exist in several levels that produce identical streams:

Level Instruction sets Used on
AVX2 AVX2, FMA, BMI1, BMI2, LZCNT, POPCNT Intel Haswell / AMD Excavator and newer, with an operating system that saves the AVX registers.
SSSE3 SSSE3 x86 CPUs without the AVX2 set (Intel Core 2 and newer, AMD Bobcat / Bulldozer and newer).
SSE2 SSE2 Other x86-64 CPUs (base level, part of every x86-64 CPU).
C++ - ARM and other platforms, 32-bit x86 builds without SSE2 code generation on CPUs without SSSE3, and builds with VENCODERJPEG_SIMD=OFF (auto-vectorized by the compiler).

The base level is compiled with the options of the library; on x86 the kernel sources are compiled once more for the SSSE3 and the AVX2 level, each with the instruction set options of that level only (GCC and Clang: -mssse3, -mavx2 -mbmi -mbmi2 -mlzcnt -mpopcnt; MSVC: /arch:AVX2 for the AVX2 level). When the first encoder is created, the library reads the CPU features (CPUID, XGETBV) and uses the fastest level the CPU and the operating system support; nothing else is compiled with instruction set options, so the library runs on every CPU of the target architecture. CMake checks that the compiler can build each level and prints the result, for example VEncoderJPEG: kernel levels: base, SSSE3 ON, AVX2 ON; a level that cannot be built is left out (private compile definitions VENCODERJPEG_NO_SSSE3, VENCODERJPEG_NO_AVX2, and VENCODERJPEG_NO_SIMD for VENCODERJPEG_SIMD=OFF). Universal macOS binaries (several CMAKE_OSX_ARCHITECTURES) use the base level.

Build on Linux

cd VEncoderJPEG
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build . -j

Executables are generated in build/bin: VEncoderJPEGTest, VEncoderJPEGExample and VEncoderJPEGBenchmark. The binaries run on every x86-64 CPU and use the AVX2 kernels where the CPU supports them.

Build on Windows

Using Developer Command Prompt for Visual Studio or PowerShell:

cd VEncoderJPEG
mkdir build; cd build
cmake -G "Visual Studio 17 2022" -A x64 ..
cmake --build . --config Release

Executables are generated in build/bin/Release.

Build and connect to project

If you want to connect the VEncoderJPEG library to your CMake project as source code, you can follow these steps. For example, if your repository has the following structure:

CMakeLists.txt
src
    CMakeLists.txt
    yourLib.h
    yourLib.cpp

Create a 3rdparty folder in your repository and copy (or add as git submodule) the VEncoderJPEG repository folder to the 3rdparty folder. The new structure of your repository:

CMakeLists.txt
src
    CMakeLists.txt
    yourLib.h
    yourLib.cpp
3rdparty
    VEncoderJPEG

Create a CMakeLists.txt file in the 3rdparty folder. The CMakeLists.txt should contain:

cmake_minimum_required(VERSION 3.13)

################################################################################
## 3RD-PARTY
## dependencies for the project
################################################################################
project(3rdparty LANGUAGES CXX)

################################################################################
## SETTINGS
## basic 3rd-party settings before use
################################################################################
# To inherit the top-level architecture when the project is used as a submodule.
SET(PARENT ${PARENT}_YOUR_PROJECT_3RDPARTY)
# Disable self-overwriting of parameters inside included subdirectories.
SET(${PARENT}_SUBMODULE_CACHE_OVERWRITE OFF CACHE BOOL "" FORCE)

################################################################################
## CONFIGURATION
## 3rd-party submodules configuration
################################################################################
SET(${PARENT}_SUBMODULE_VENCODERJPEG                    ON  CACHE BOOL "" FORCE)
if (${PARENT}_SUBMODULE_VENCODERJPEG)
    SET(${PARENT}_VENCODERJPEG                          ON  CACHE BOOL "" FORCE)
    SET(${PARENT}_VENCODERJPEG_TEST                     OFF CACHE BOOL "" FORCE)
    SET(${PARENT}_VENCODERJPEG_EXAMPLE                  OFF CACHE BOOL "" FORCE)
    SET(${PARENT}_VENCODERJPEG_BENCHMARK                OFF CACHE BOOL "" FORCE)
endif()

################################################################################
## INCLUDING SUBDIRECTORIES
## Adding subdirectories according to the 3rd-party configuration
################################################################################
if (${PARENT}_SUBMODULE_VENCODERJPEG)
    add_subdirectory(VEncoderJPEG)
endif()

The 3rdparty/CMakeLists.txt file adds the VEncoderJPEG folder to your project and excludes the test applications, the example and the benchmark from compilation (they are also excluded by default when VEncoderJPEG is included as a submodule). Next, include the 3rdparty folder in the main CMakeLists.txt file of your repository by adding at the end:

add_subdirectory(3rdparty)

Next, link the VEncoderJPEG library in your src/CMakeLists.txt file:

target_link_libraries(${PROJECT_NAME} VEncoderJPEG)

Done! Include the header in your code:

#include "VEncoderJPEG.h"

Simple example

This example (example/main.cpp) generates frames in NV12 pixel format, encodes them and saves the first frame as example.jpg. Source code:

#include <cstdint>
#include <fstream>
#include <iostream>
#include <memory>
#include "VEncoderJPEG.h"



int main()
{
    std::cout << "VEncoderJPEG v" << cr::video::VEncoderJPEG::getVersion() << " example" <<
    std::endl;

    // Create codec object and set params.
    std::unique_ptr<cr::video::VCodec> encoder{std::make_unique<cr::video::VEncoderJPEG>()};
    encoder->setParam(cr::video::VCodecParam::QUALITY, 80);
    // Automatic number of threads.
    encoder->setParam(cr::video::VCodecParam::CUSTOM_2, 0);

    // Image size.
    const int width{640};
    const int height{480};
    // Source frame.
    cr::video::Frame srcFrame{width, height, cr::video::Fourcc::NV12};
    // Result frame JPEG.
    cr::video::Frame dstFrame{width, height, cr::video::Fourcc::JPEG};

    // Main loop.
    for (int n = 0; n < 100; ++n)
    {
        // Fill NV12 frame: moving luma gradient and constant colour plane.
        uint8_t* luma{srcFrame.data};
        uint8_t* chroma{srcFrame.data + width * height};
        for (int y = 0; y < height; ++y)
        {
            for (int x = 0; x < width; ++x)
            {
                luma[y * width + x] = static_cast<uint8_t>(x + y + 4 * n);
            }
        }
        for (int y = 0; y < height / 2; ++y)
        {
            for (int x = 0; x < width / 2; ++x)
            {
                chroma[y * width + 2 * x] = static_cast<uint8_t>(64 + x / 4);
                chroma[y * width + 2 * x + 1] = static_cast<uint8_t>(192 - y / 4);
            }
        }
        srcFrame.frameId = n;

        // Encode data.
        if (!encoder->transcode(srcFrame, dstFrame))
        {
            std::cout << "Can't encode frame" << std::endl;
            continue;
        }

        // Show info.
        const int ratio{static_cast<int>(100.0f * static_cast<float>(dstFrame.size) /
                                         static_cast<float>(srcFrame.size))};
        std::cout << "[" << dstFrame.frameId << "] Size " << dstFrame.size <<
        " bytes, compression ratio : %" << ratio << std::endl;

        // Save first frame as a JPEG file.
        if (n == 0)
        {
            std::ofstream file{"example.jpg", std::ios::binary};
            file.write(reinterpret_cast<const char*>(dstFrame.data), dstFrame.size);
        }
    }

    return 0;
}

Test application

The test application (test/main.cpp) generates artificial video (colour gradients, texture and a moving object) in any supported pixel format, encodes it with the parameters chosen by the user and writes the result to out.mjpeg (all frames, playable with ffplay -f mjpeg out.mjpeg) and out.jpg (first frame). The application shows the encoded size and encoding time of every frame and a summary at the end. To run the application on Linux:

cd <application folder>
./VEncoderJPEGTest

After starting, you will see the following output:

====================================
VEncoderJPEG v1.0.1 test
====================================

Default params:
Video width 1280
Video height 720
Pixel format: NV12
Quality: 80
Subsampling (0 - auto, 1 - 4:2:0, 2 - 4:2:2, 3 - 4:4:4, 4 - gray): 0
Threads (0 - auto, 1 - single thread, N - N threads): 1
Use default params (0 - no, 1 - yes) : 1
Set number frames: 100
Set noise mode (0 - no noise, 1 - noise) : 0

If default params are not used, the application asks for video width, height, pixel format (0 - NV12, 1 - NV21, 2 - YU12, 3 - YV12, 4 - YUYV, 5 - UYVY, 6 - RGB24, 7 - BGR24, 8 - YUV24, 9 - GRAY), quality, subsampling and number of threads. During encoding the application shows the source and encoded size and the encoding time:

Data size 1382400 / 81534   encoding time  1043 us
Data size 1382400 / 82121   encoding time  834 us
Data size 1382400 / 82213   encoding time  892 us
...
Frames encoded: 100
Encoding time, us: average 810, min 743, max 1171 (1234 fps)
Average frame size: 82017 bytes (5.93296 % of source)
Output: out.mjpeg, first frame: out.jpg

All parameters can also be given in the command line (non-interactive mode); width and height must be 16 - 65535 pixels with width * height * 4 not exceeding 2147483647:

./VEncoderJPEGTest <width> <height> <format> <quality> <subsampling> <threads> <frames> <noise>
./VEncoderJPEGTest 1920 1080 7 90 0 0 300 1

Benchmark

The benchmark application (benchmark/main.cpp, executable VEncoderJPEGBenchmark) measures the encoding time for frame sizes 640x480, 1280x720 and 1920x1080 at quality 80 and 100; all other parameters keep their defaults (NV12 source, automatic subsampling - 4:2:0, one thread). The source is the synthetic scene of the test application (colour gradients, fine texture and a moving object) rendered in NV12. No files are written. For every test the application prints its stages (source preparation, encoder creation, warm-up, encoding with progress, short result) and at the end a markdown table with the frame size, quality, number of measured frames and the average, minimum and maximum time of the transcode() call. To run:

cd <application folder>
./VEncoderJPEGBenchmark            # 200 measured frames per test (+10 warm-up frames)
./VEncoderJPEGBenchmark 1000       # custom number of measured frames (1 - 1000000)
taskset -c 0 ./VEncoderJPEGBenchmark   # Linux: pin to one core for stable results

Output example (12th Gen Intel Core i5-12500H, WSL2, pinned to one core):

====================================
VEncoderJPEG v1.0.1 benchmark
====================================

Settings:
  Source: synthetic scene of the test application, NV12
  Qualities: 80, 100
  Subsampling (CUSTOM_1): 0 (auto: 4:2:0 for NV12)
  Threads (CUSTOM_2): 1
  Range mode (CUSTOM_3): 0
  Frames per test: 200 (+ 10 warm-up frames)
  Logical processors: 16

[1/6] 640x480, quality 80
      Preparing source frames (NV12, 640x480)... done
      Creating encoder (quality 80, other params default)... done
      Warm-up: 10 frames... done (first frame 0.311 ms)
      Encoding: 200 frames... 25% 50% 75% 100% done
      Result: avg 0.253 ms, min 0.232 ms, max 0.637 ms
...
Results (encoding time per frame, transcode() call only):
Size Quality Frames Avg, ms Min, ms Max, ms
640x480 80 200 0.253 0.232 0.637
640x480 100 200 0.577 0.507 1.413
1280x720 80 200 0.726 0.686 1.027
1280x720 100 200 1.661 1.513 2.551
1920x1080 80 200 1.716 1.515 2.463
1920x1080 100 200 3.611 3.307 5.179

The synthetic scene is easier to compress than camera images (see Quality and performance for measurements on a real camera frame). On hybrid CPUs, laptops and virtual machines single frames may take much longer because of thread migration and power management, which shows in the maximum; pin the benchmark to one core for repeatable results.

Testing

The test application, the example and the benchmark use the public interface only and are built with the same warning and hardening options as the library. Encoded streams can be checked with any JPEG decoder, for example:

ffmpeg -v error -xerror -f mjpeg -i out.mjpeg -f null -

Builds with AddressSanitizer and UndefinedBehaviorSanitizer (GCC or Clang on Linux):

cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" ..
cmake --build . -j
./bin/VEncoderJPEGTest 1920 1080 6 90 3 0 100 1

For data races use -fsanitize=thread instead and automatic threads (the sixth argument 0). On WSL2 and some other kernels the sanitizer runtimes need address space randomization switched off: setarch $(uname -m) -R ./bin/VEncoderJPEGTest ….

The kernel levels below the one of the build machine are exercised with CPU emulation, for example qemu-x86_64 -cpu Nehalem ./bin/VEncoderJPEGTest … (SSSE3 level) or -cpu qemu64 (SSE2 level), or with the build options VENCODERJPEG_AVX2=OFF and VENCODERJPEG_SIMD=OFF. All levels produce identical streams.

Quality and performance

Measurements: 12th Gen Intel Core i5-12500H, Ubuntu 24.04 (WSL2), GCC 13.3, default build (AVX2 kernel level). Test image: real 1920x1080 camera image.

Table 4 - NV12 source (YCbCr 4:2:0). PSNR of the decoded luma plane against the source luma plane.

Quality Size, bytes PSNR Y, dB
30 85797 35.262
50 140975 36.602
75 250071 38.501
90 487662 41.514
95 803547 44.463
100 1809274 58.874

Table 5 - RGB24 source (colour conversion and chroma downsampling included). PSNR of decoded RGB against the source.

Subsampling Quality Size, bytes PSNR, dB
4:2:0 50 141030 31.076
4:2:0 75 250074 32.089
4:2:0 90 487627 33.905
4:2:2 90 589737 35.417
4:4:4 90 757682 36.990
4:4:4 100 3266798 50.774

Table 6 - Encoding time, ms per frame (4:2:0; best of 5 series of 10 frames).

Frame size Source, quality 1 thread 4 threads Auto threads
1920x1080 NV12, 80 2.7 0.97 0.75
1920x1080 BGR24, 80 3.6 1.2 0.93
1920x1080 NV12, 95 5.0 1.8 1.35
1280x720 NV12, 80 1.2 0.42 0.42
1280x720 BGR24, 80 1.6 0.56 0.47
1280x720 NV12, 95 2.0 0.83 0.73
640x512 NV12, 80 0.35 0.16 0.15
640x512 BGR24, 80 0.47 0.18 0.19
640x512 NV12, 95 0.59 0.25 0.25

Table of contents