Create Video From JPEG Images¶
This topic describes how to use the TTranscoder class to make a video from a sequence of images.
The code snippets in this article are from the slideshow sample.
Create a Transcoder object¶
Create a new TTranscoder object and enable the demo mode.
TTranscoder transcoder;
transcoder.allowDemoMode(true);
Configure inputs¶
Detect the input stream¶
If the input is a media file, you can use TMediaInfo to read the stream information from the file. For images TMediaInfo will return a TVideoStreamInfo object readily configured with the image attributes. AVBlocks supports BMP, JPEG, PNG, and TIFF images.
TMediaInfo mediaInfo;
std::string firstImage = std::string(opt.input_dir) + "/cube0000.jpeg";
mediaInfo.inputs(0).file(firstImage);
mediaInfo.open();
Configure the input socket¶
This requires the following steps:
Get a
TVideoStreamInfoobject fromTMediaInfoCreate a
TMediaPinobject and call theTMediaPin::streamInfomethod.Create a
TMediaSocketobject and add the pin withTMediaSocket::addPin.Add the socket to the transcoder with
TTranscoder::addInput.
auto vsi = mediaInfo.outputs(0).pins(0).videoStreamInfo().clone();
vsi.frameRate(inputFramerate);
transcoder.addInput(
TMediaSocket()
.addPin(TMediaPin().streamInfo(vsi))
);
Configure outputs¶
This requires the following steps:
Create a
TMediaSocketobject from a predefined preset by passing the preset name to its constructorCall the
TMediaSocket::filemethod with the output filenameAdd the socket to the transcoder with
TTranscoder::addOutput
TMediaSocket(opt.preset.name)
.file(opt.output_file)
Encode images¶
This requires the following steps:
Call
TTranscoder::openCall
TTranscoder::pushfor each imageCall
TTranscoder::flush(always needed after pushing)Call
TTranscoder::close
transcoder.open();
if (!transcoder.flush())
{
printError("Flush Transcoder", transcoder.error());
transcoder.close();
return false;
}
transcoder.close();
std::println("Output video: \"{}\"", opt.output_file);
Push image data¶
The code that pushes the image data is fairly simple.
For each image:
Create a
TMediaSampleobjectCreate a
TMediaBufferobject and load it with the image dataSet the sample start time with
TMediaSample::startTime, and set the sample buffer withTMediaSample::bufferCall
TTranscoder::pushwith the media sample object
TMediaSample sample;
for (int i = 0; i < imageCount; ++i)
{
auto imagePath = std::format("{}/cube{:04d}.jpeg", opt.input_dir, i);
TMediaBuffer buffer = loadImageFile(imagePath.c_str());
sample.buffer(std::move(buffer));
sample.startTime(i / inputFramerate);
if (!transcoder.push(0, sample))
{
printError("Push Transcoder", transcoder.error());
transcoder.close();
return false;
}
}
static TMediaBuffer loadImageFile(const char* filename)
{
std::ifstream file(filename, std::ios::binary | std::ios::ate);
if (!file)
{
std::println(stderr, "Cannot open image file: {}", filename);
return TMediaBuffer(0);
}
auto imgsize = static_cast<int32_t>(file.tellg());
file.seekg(0);
TMediaBuffer buffer(imgsize);
file.read(reinterpret_cast<char*>(buffer.start()), imgsize);
buffer.setData(0, static_cast<int32_t>(file.gcount()));
return buffer;
}
When you are done pushing, call TTranscoder::flush to make sure all buffered data is processed.
if (!transcoder.flush())
{
printError("Flush Transcoder", transcoder.error());
transcoder.close();
return false;
}
Complete C++ code¶
Here is the complete sample:
#include <primo/avblocks/avb++.h>
#include <fstream>
#include <print>
#include <string>
#include "options.h"
#include "util.h"
using namespace primo::codecs;
using namespace primo::avblocks::modern;
static TMediaBuffer loadImageFile(const char* filename)
{
std::ifstream file(filename, std::ios::binary | std::ios::ate);
if (!file)
{
std::println(stderr, "Cannot open image file: {}", filename);
return TMediaBuffer(0);
}
auto imgsize = static_cast<int32_t>(file.tellg());
file.seekg(0);
TMediaBuffer buffer(imgsize);
file.read(reinterpret_cast<char*>(buffer.start()), imgsize);
buffer.setData(0, static_cast<int32_t>(file.gcount()));
return buffer;
}
bool slideshow(const Options& opt)
{
std::println("preset: {}", opt.preset.name);
const double inputFramerate = 25.0;
const int imageCount = 250;
deleteFile(opt.output_file.c_str());
try {
// Probe the first image to get frame dimensions
TMediaInfo mediaInfo;
std::string firstImage = std::string(opt.input_dir) + "/cube0000.jpeg";
mediaInfo.inputs(0).file(firstImage);
mediaInfo.open();
// Build input socket: no file — we will push JPEG frames manually
auto vsi = mediaInfo.outputs(0).pins(0).videoStreamInfo().clone();
vsi.frameRate(inputFramerate);
TTranscoder transcoder;
transcoder.allowDemoMode(true);
transcoder.addInput(
TMediaSocket()
.addPin(TMediaPin().streamInfo(vsi))
);
// Output via preset
transcoder.addOutput(
TMediaSocket(opt.preset.name)
.file(opt.output_file)
);
transcoder.open();
TMediaSample sample;
for (int i = 0; i < imageCount; ++i)
{
auto imagePath = std::format("{}/cube{:04d}.jpeg", opt.input_dir, i);
TMediaBuffer buffer = loadImageFile(imagePath.c_str());
sample.buffer(std::move(buffer));
sample.startTime(i / inputFramerate);
if (!transcoder.push(0, sample))
{
printError("Push Transcoder", transcoder.error());
transcoder.close();
return false;
}
}
if (!transcoder.flush())
{
printError("Flush Transcoder", transcoder.error());
transcoder.close();
return false;
}
transcoder.close();
std::println("Output video: \"{}\"", opt.output_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 slideshow(opt) ? 0 : 1;
}