Skip to content

Instantly share code, notes, and snippets.

@ZenulAbidin
Last active February 17, 2021 09:29
Show Gist options
  • Save ZenulAbidin/4fb0047e459bd929a7c02784f78daca1 to your computer and use it in GitHub Desktop.
Save ZenulAbidin/4fb0047e459bd929a7c02784f78daca1 to your computer and use it in GitHub Desktop.
My GIFExporter class for use with ffmpeg (version 4 only?) and Qt5. Also supports cropping all the image frames before encoding. Licensed under dual BSD/MIT license.
#include <QtWidgets>
#include <iostream>
#include <cstdlib>
// extern "C" prevents linker errors when using this in a C++ program.
// Remove 'extern "C" {' if putting this in a C program.
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
}
int ERROR_BUFSIZ = 1024;
int64_t nextPts = 0;
class GIFExporter {
public:
GIFExporter()
{
errorstring = new char[ERROR_BUFSIZ];
}
// Call this before begining exporting.
void init(const QString &fileName, bool crop, QRect rect)
{
int error;
this->fileName = fileName;
// Allows you to crop each image with the same rectangle if you want
this->crop = crop;
this->rect = rect;
// Make sure fileName ends with .gif
// if we get errors here maybe it assigned the wrong codec
error = avformat_alloc_output_context2(&this->formatContext, NULL, NULL, fileName.toStdString().data());
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
// Adding the video streams using the default format codecs and initializing the codecs...
this->outputFormat = this->formatContext->oformat;
if (this->outputFormat->video_codec != AV_CODEC_ID_NONE) {
// Finding a registered encoder with a matching codec ID...
this->codec = avcodec_find_encoder(this->outputFormat->video_codec);
if (this->codec == NULL) {
// It doesn't return an error number
std::cerr << __FILE__ << ":" << __LINE__ << " " << "Encoder not found" << std::endl;
exit(1);
}
// Adding a new stream to a media file...
this->stream = avformat_new_stream(this->formatContext, this->codec);
if (this->stream == NULL) {
// It doesn't return an error number
std::cerr << __FILE__ << ":" << __LINE__ << " " << "Could not create new AVStream" << std::endl;
exit(1);
}
this->stream->id = this->formatContext->nb_streams - 1;
this->codecContext = avcodec_alloc_context3(this->codec);
if (this->codecContext == NULL) {
// It doesn't return an error number
std::cerr << __FILE__ << ":" << __LINE__ << " " << "Could not allocate AVContext" << std::endl;
exit(1);
}
switch (this->codec->type) {
case AVMEDIA_TYPE_VIDEO:
this->codecContext->codec_id = this->outputFormat->video_codec; // here, outputFormat->video_codec should be AV_CODEC_ID_GIF
this->codecContext->bit_rate = 400000;
this->codecContext->width = (rect.width() & (~1));
this->codecContext->height = (rect.height() & (~1));
this->codecContext->pix_fmt = AV_PIX_FMT_RGB8;
// Timebase: this is the fundamental unit of time (in seconds) in terms of which frame
// timestamps are represented. For fixed-fps content, timebase should be 1/framerate
// and timestamp increments should be identical to 1.
// I needed to multiply this by 3 because for some reason, the GIF was 3x slower than normal.
// GIF frame rates have a hard limit of 100FPS. If FRAME_RATE=30 i.e. 30FPS then this workaround actually encodes it at 90FPS
// If however the GIF moves too fast because of this then remove the *3 from the next line.
this->stream->time_base = (AVRational){1, FRAME_RATE*3};
this->codecContext->time_base = this->stream->time_base;
// Intra frames only, no groups of frames. Bad for compression but I don't want to handle receiving extra frames here,
// And I don't think GIF codec can even emit groups of frames.
this->codecContext->gop_size = 0;
break;
case AVMEDIA_TYPE_UNKNOWN:
case AVMEDIA_TYPE_AUDIO:
case AVMEDIA_TYPE_DATA:
case AVMEDIA_TYPE_SUBTITLE:
case AVMEDIA_TYPE_ATTACHMENT:
case AVMEDIA_TYPE_NB:
std::cerr << __FILE__ << ":" << __LINE__ << " " << "Invalid media type " << this->codec->type << std::endl;
exit(1);
break;
}
if (this->formatContext->oformat->flags & AVFMT_GLOBALHEADER) {
this->codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
}
// This isn't thread-safe.
error = avcodec_open2(this->codecContext, this->codec, NULL);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
error = avcodec_parameters_from_context(this->stream->codecpar, this->codecContext);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
av_dump_format(this->formatContext, 0, fileName.toStdString().data(), 1);
if (!(this->outputFormat->flags & AVFMT_NOFILE)) {
// it should not have AVFMT_NOFILE
error = avio_open(&this->formatContext->pb, fileName.toStdString().data(), AVIO_FLAG_WRITE);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
}
// Writing the stream header, if any...
error = avformat_write_header(this->formatContext, NULL);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
else if (error == AVSTREAM_INIT_IN_WRITE_HEADER) {
std::cerr << "AVSTREAM_INIT_IN_WRITE_HEADER" << std::endl; // informational message, not an error
}
else if (error == AVSTREAM_INIT_IN_INIT_OUTPUT) {
std::cerr << "AVSTREAM_INIT_IN_INIT_OUTPUT" << std::endl; // informational message, not an error
}
}
void addframe(const QImage &img, int framenumber)
{
int error;
// If framenumber is not incremented in sequential order (with first frame having framenumber=1), then this will return without encoding the rest of the frames.
// This is a relic from my program where I take images from a QOpenGLWindow using the grabFrameBuffer() method and it's important that in the event that OpenGL
// drops frames, frame encoding is aborted instead of missing dropped frames from the GIF.
// In particular, QOpenGLWindow paints at 60FPS
if (nextPts == framenumber) {
return; // same frame, nothing to do
}
nextPts += 1;
QImage cropped;
if (this->crop) {
cropped = img.copy(this->rect);
}
else {
cropped = img;
}
// Make sure that your image and this->rect have the same dimensions
// Odd numbered GIF widths/heights are known to cause errors in the YUV420P encoding process making the GIF slightly yellow.
// so the extra pixel length/width is chopped
const qint32 width = cropped.width() & (~1);
const qint32 height = cropped.height() & (~1);
// Here we need 3 frames for encoding. Basically, the QImage is firstly extracted in AV_PIX_FMT_BGRA.
// We need then to convert it to AV_PIX_FMT_RGB8 which is required by the .gif format.
// If we do that directly, there will be some artefacts and bad effects... to prevent that
// we convert FIRST AV_PIX_FMT_BGRA into AV_PIX_FMT_YUV420P THEN into AV_PIX_FMT_RGB8.
AVFrame * frame = av_frame_alloc();
frame->width = this->codecContext->width;
frame->height = this->codecContext->height;
frame->format = this->codecContext->pix_fmt;
AVFrame * tmpFrame = av_frame_alloc();
tmpFrame->width = this->codecContext->width;
tmpFrame->height = this->codecContext->height;
tmpFrame->format = AV_PIX_FMT_BGRA;
AVFrame * yuvFrame = av_frame_alloc();
yuvFrame->width = codecContext->width;
yuvFrame->height = codecContext->height;
yuvFrame->format = AV_PIX_FMT_YUV420P;
error = av_frame_get_buffer(tmpFrame, 0);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
error = av_image_alloc(tmpFrame->data, tmpFrame->linesize, width, height, AV_PIX_FMT_BGRA, 32);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
// When we pass a frame to the encoder, it may keep a reference to it internally;
// make sure we do not overwrite it here!
error = av_frame_make_writable(tmpFrame);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
// Converting QImage to AV_PIX_FMT_BGRA AVFrame ...
for (qint32 y = 0; y < height; y++) {
const uint8_t * scanline = cropped.scanLine(y);
for (qint32 x = 0; x < width * 4; x++) {
tmpFrame->data[0][y * tmpFrame->linesize[0] + x] = scanline[x];
}
}
// Make sure to clear the frame. It prevents a bug that displays only the
// first captured frame on the GIF export.
if (frame) {
av_frame_free(&frame);
frame = Q_NULLPTR;
}
frame = av_frame_alloc();
frame->width = codecContext->width;
frame->height = codecContext->height;
frame->format = codecContext->pix_fmt;
error = av_image_alloc(frame->data, frame->linesize, width, height, codecContext->pix_fmt, 32);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
if (yuvFrame) {
av_frame_free(&yuvFrame);
yuvFrame = Q_NULLPTR;
}
yuvFrame = av_frame_alloc();
yuvFrame->width = codecContext->width;
yuvFrame->height = codecContext->height;
yuvFrame->format = AV_PIX_FMT_YUV420P;
error = av_image_alloc(yuvFrame->data, yuvFrame->linesize, width, height, AV_PIX_FMT_YUV420P, 32);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
// Converting BGRA -> YUV420P...
struct SwsContext * swsCtx = Q_NULLPTR;
struct SwsContext * swsGIFCtx = Q_NULLPTR;
if (!swsCtx) {
swsCtx = sws_getContext(width, height,
AV_PIX_FMT_BGRA,
width, height,
AV_PIX_FMT_YUV420P,
SWS_BICUBIC, NULL, NULL, NULL);
if (swsCtx == NULL) {
// It doesn't return an error number
std::cerr << __FILE__ << ":" << __LINE__ << " " << "Could not allocate SwsContext swsCtx" << std::endl;
exit(1);
}
}
// ...then converting YUV420P -> RGB8 (natif GIF format pixel)
if (!swsGIFCtx) {
swsGIFCtx = sws_getContext(width, height,
AV_PIX_FMT_YUV420P,
codecContext->width, codecContext->height,
codecContext->pix_fmt,
SWS_BICUBIC, NULL, NULL, NULL);
if (swsGIFCtx == NULL) {
// It doesn't return an error number
std::cerr << __FILE__ << ":" << __LINE__ << " " << "Could not allocate SwsContext swsGIFCtx" << std::endl;
exit(1);
}
}
// This double scaling prevent some artifacts on the GIF and improve
// significantly the display quality
sws_scale(swsCtx,
(const uint8_t * const *)tmpFrame->data,
tmpFrame->linesize,
0,
codecContext->height,
yuvFrame->data,
yuvFrame->linesize);
sws_scale(swsGIFCtx,
(const uint8_t * const *)yuvFrame->data,
yuvFrame->linesize,
0,
codecContext->height,
frame->data,
frame->linesize);
AVPacket *packet = av_packet_alloc();
if (packet == NULL) {
// It doesn't return an error number
std::cerr << __FILE__ << ":" << __LINE__ << " " << "Could not allocate AVPacket" << std::endl;
exit(1);
}
av_init_packet(packet);
// Packet data will be allocated by the encoder
packet->data = NULL;
packet->size = 0;
frame->pts = nextPts++; // nextPts starts at 0
error = avcodec_send_frame(this->codecContext, frame);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
error = avcodec_receive_packet(this->codecContext, packet);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
// Rescale output packet timestamp values from codec to stream timebase
av_packet_rescale_ts(packet, codecContext->time_base, this->stream->time_base);
packet->stream_index = this->stream->index;
// Write the compressed frame to the media file.
error = av_interleaved_write_frame(formatContext, packet);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
// free packets
av_freep(&tmpFrame->data[0]);
av_freep(&yuvFrame->data[0]);
av_freep(&frame->data[0]);
av_frame_free(&frame);
av_frame_free(&tmpFrame);
av_frame_free(&yuvFrame);
sws_freeContext(swsCtx);
sws_freeContext(swsGIFCtx);
av_packet_free(&packet);
}
// Call this after all the frames were encoded to write them to the file and close it.
void commitFile()
{
int error;
nextPts = 0;
error = av_write_trailer(this->formatContext);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
avcodec_free_context(&this->codecContext);
if (!(this->outputFormat->flags & AVFMT_NOFILE)) {
// Closing the output file...
error = avio_closep(&this->formatContext->pb);
if (error < 0) {
av_strerror(error, this->errorstring, ERROR_BUFSIZ);
std::cerr << __FILE__ << ":" << __LINE__ << " " << this->errorstring << std::endl;
exit(1);
}
}
avformat_free_context(this->formatContext);
}
private:
QString fileName;
bool crop;
QRect rect;
AVFormatContext * formatContext;
AVCodec * codec;
AVCodecContext * codecContext;
AVOutputFormat * outputFormat;
AVStream * stream;
char *errorstring;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment