From ed703e6a567e38f98e4dcf3b36321740060f572d Mon Sep 17 00:00:00 2001 From: Sam Hocevar Date: Tue, 25 Oct 2016 14:00:20 +0200 Subject: [PATCH] doc: add a GIF exporting program (experimental for now) --- .gitignore | 1 + doc/tutorial/16_movie.cpp | 234 +++++++++++++++++++++++++++++++++++++- 2 files changed, 234 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3c8ae75b..37d715c0 100644 --- a/.gitignore +++ b/.gitignore @@ -102,6 +102,7 @@ doc/tutorial/13_shader_builder doc/tutorial/14_lol_lua doc/tutorial/15_lolimgui doc/tutorial/16_movie +doc/tutorial/16_movie.gif tools/make-font # Our data doc/doxygen.cfg diff --git a/doc/tutorial/16_movie.cpp b/doc/tutorial/16_movie.cpp index e7b8c1fc..87ccea62 100644 --- a/doc/tutorial/16_movie.cpp +++ b/doc/tutorial/16_movie.cpp @@ -1,5 +1,5 @@ // -// Lol Engine — Movie tutorial +// Lol Engine — GIF encoding sample // // Copyright © 2016 Sam Hocevar // @@ -15,11 +15,243 @@ #endif #include +#include "loldebug.h" + +extern "C" { +#include +#include +#include +#include +#include +#include +#include +} using namespace lol; +class gif_encoder +{ +public: + gif_encoder(ivec2 size) + : m_avformat(nullptr), + m_avcodec(nullptr), + m_stream(nullptr), + m_frame(nullptr), + m_size(size), + m_index(0) + { + av_register_all(); + av_log_set_callback(ffmpeg_logger); + + m_frame = av_frame_alloc(); + ASSERT(m_frame); + + m_frame->format = AV_PIX_FMT_RGB8; // 3:3:2 packed for GIF + m_frame->width = m_size.x; + m_frame->height = m_size.y; + + int ret = av_frame_get_buffer(m_frame, 32); + ASSERT(ret >= 0); + } + + bool open_file(char const *filename) + { + avformat_alloc_output_context2(&m_avformat, nullptr, nullptr, filename); + if (!m_avformat) + { + msg::debug("could not deduce output format from file extension %s: using GIF\n", filename); + avformat_alloc_output_context2(&m_avformat, nullptr, "gif", filename); + + if (!m_avformat) + return false; + } + + if (!open_codec()) + return false; + + if (!(m_avformat->oformat->flags & AVFMT_NOFILE)) + { + int ret = avio_open(&m_avformat->pb, filename, AVIO_FLAG_WRITE); + if (ret < 0) + { + msg::error("could not open '%s': %s\n", filename, error2string(ret).C()); + return false; + } + } + + int ret = avformat_write_header(m_avformat, nullptr); + if (ret < 0) + { + msg::error("could not write header: %s\n", error2string(ret).C()); + return false; + } + + return true; + } + + bool push_image(Image &im) + { + // Make sure the encoder does not hold a reference on our + // frame (GIF does that in order to compress using deltas). + if (av_frame_make_writable(m_frame) < 0) + return false; + + // Convert image to 3:3:2. TODO: add some dithering + u8vec3 *data = im.Lock(); + for (int n = 0; n < im.GetSize().x * im.GetSize().y; ++n) + m_frame->data[0][n] = (data[n].r & 0xe0) | ((data[n].g & 0xe0) >> 3) | (data[n].b >> 6); + im.Unlock(data); + + m_frame->pts = m_index++; + + AVPacket pkt; + memset(&pkt, 0, sizeof(pkt)); + av_init_packet(&pkt); + + // XXX: is got_packet necessary? + int got_packet = 0; + int ret = avcodec_encode_video2(m_avcodec, &pkt, m_frame, &got_packet); + if (ret < 0) + { + msg::error("cannot encode video frame: %s\n", error2string(ret).C()); + return false; + } + + if (got_packet) + { + pkt.stream_index = m_stream->index; + + ret = av_interleaved_write_frame(m_avformat, &pkt); + if (ret < 0) + { + msg::error("cannot write video frame: %s\n", error2string(ret).C()); + return false; + } + } + + return true; + } + + void close() + { + // this must be done before m_avcodec is freed + av_write_trailer(m_avformat); + + avcodec_free_context(&m_avcodec); + av_frame_free(&m_frame); + + if (!(m_avformat->oformat->flags & AVFMT_NOFILE)) + avio_closep(&m_avformat->pb); + + avformat_free_context(m_avformat); + } + +private: + bool open_codec() + { + AVCodec *codec = avcodec_find_encoder(m_avformat->oformat->video_codec); + if (!codec) + { + msg::error("no encoder found for %s\n", avcodec_get_name(m_avformat->oformat->video_codec)); + return false; + } + + m_stream = avformat_new_stream(m_avformat, nullptr); + if (!m_stream) + { + msg::error("cannot allocate stream\n"); + return false; + } + m_stream->id = 0; // first (and only?) stream + m_stream->time_base = AVRational{ 1, 30 }; // 30 fps + + m_avcodec = avcodec_alloc_context3(codec); + if (!m_avcodec) + { + msg::error("cannot allocate encoding context\n"); + return false; + } + + m_avcodec->codec_id = m_avformat->oformat->video_codec; + m_avcodec->width = m_frame->width; + m_avcodec->height = m_frame->height; + m_avcodec->pix_fmt = AVPixelFormat(m_frame->format); + m_avcodec->time_base = m_stream->time_base; + + if (m_avformat->oformat->flags & AVFMT_GLOBALHEADER) + m_avcodec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + + int ret = avcodec_open2(m_avcodec, codec, nullptr); + if (ret < 0) + { + msg::error("cannot open video codec: %s\n", error2string(ret).C()); + return false; + } + + ret = avcodec_parameters_from_context(m_stream->codecpar, m_avcodec); + if (ret < 0) + { + msg::error("cannot copy stream parameters\n"); + return false; + } + + return true; + } + + static String error2string(int errnum) + { + char tmp[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(errnum, tmp, AV_ERROR_MAX_STRING_SIZE); + return String(tmp); + } + + static void ffmpeg_logger(void *ptr, int level, const char *fmt, va_list vl) + { + // FIXME: use lol::msg::debug + UNUSED(ptr, level); + vfprintf(stderr, fmt, vl); + } + +private: + AVFormatContext *m_avformat; + AVCodecContext *m_avcodec; + AVStream *m_stream; + AVFrame *m_frame; + ivec2 m_size; + int m_index; +}; + + int main(int argc, char **argv) { + UNUSED(argc, argv); + + ivec2 size(256, 256); + + gif_encoder enc(size); + if (!enc.open_file("16_movie.gif")) + return EXIT_FAILURE; + + for (int i = 0; i < 256; ++i) + { + Image im(size); + + array2d &data = im.Lock2D(); + for (int y = 0; y < size.y; ++y) + for (int x = 0; x < size.x; ++x) + { + data[x][y].r = x * i / 2; + data[x][y].g = x / 4 * 4 * y / 16 + i; + data[x][y].b = y + i; + } + im.Unlock2D(data); + + if (!enc.push_image(im)) + break; + } + + enc.close(); + return 0; }