From c6374c7e073589b7e8fd3c6663e2673eb2e67508 Mon Sep 17 00:00:00 2001 From: Sam Hocevar Date: Tue, 13 Jun 2017 16:54:06 +0200 Subject: [PATCH] movie: replace the old Movie class with a GIF encoder. --- doc/tutorial/16_movie.cpp | 217 ++------------------------------------ src/image/movie.cpp | 187 ++++++++++++++++++++++++++++---- src/lol/image/movie.h | 37 ++++--- 3 files changed, 199 insertions(+), 242 deletions(-) diff --git a/doc/tutorial/16_movie.cpp b/doc/tutorial/16_movie.cpp index a1da7af9..e9feb3ea 100644 --- a/doc/tutorial/16_movie.cpp +++ b/doc/tutorial/16_movie.cpp @@ -17,224 +17,21 @@ #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) - { - /* Third argument specifies format */ - avformat_alloc_output_context2(&m_avformat, nullptr, "gif", filename); - if (!m_avformat) - { - msg::debug("could not create output context"); - 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.size().x * im.size().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); + lol::ivec2 size(256, 256); - gif_encoder enc(size); - if (!enc.open_file("16_movie.gif")) + lol::movie movie(size); + if (!movie.open_file("16_movie.gif")) return EXIT_FAILURE; for (int i = 0; i < 256; ++i) { - image im(size); + lol::image im(size); - array2d &data = im.lock2d(); + lol::array2d &data = im.lock2d(); for (int y = 0; y < size.y; ++y) for (int x = 0; x < size.x; ++x) { @@ -244,11 +41,11 @@ int main(int argc, char **argv) } im.unlock2d(data); - if (!enc.push_image(im)) + if (!movie.push_image(im)) break; } - enc.close(); + movie.close(); return 0; } diff --git a/src/image/movie.cpp b/src/image/movie.cpp index 8824de16..5c536d3c 100644 --- a/src/image/movie.cpp +++ b/src/image/movie.cpp @@ -1,7 +1,7 @@ // // Lol Engine // -// Copyright © 2010—2016 Sam Hocevar +// Copyright © 2010—2017 Sam Hocevar // // Lol Engine is free software. It comes without any warranty, to // the extent permitted by applicable law. You can redistribute it @@ -15,7 +15,13 @@ #if LOL_USE_FFMPEG extern "C" { +# include +# include +# include +# include # include +# include +# include } #endif @@ -23,49 +29,194 @@ namespace lol { #if LOL_USE_FFMPEG -static bool g_ready = false; +#define ERROR_TO_STRING(errnum) (error2string((errnum)).C()) +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); +} #endif -class MovieData +movie::movie(ivec2 size) + : m_avformat(nullptr), + m_avcodec(nullptr), + m_stream(nullptr), + m_frame(nullptr), + m_size(size), + m_index(0) { #if LOL_USE_FFMPEG + av_register_all(); + //av_log_set_callback(ffmpeg_logger); -#endif + m_frame = av_frame_alloc(); + ASSERT(m_frame); - friend class Movie; -}; + 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; -/* - * Public Movie class - */ + int ret = av_frame_get_buffer(m_frame, 32); + ASSERT(ret >= 0); +#endif +} -Movie::Movie(String const &name, ivec2 size, float fps) - : m_data(new MovieData()) +bool movie::open_file(char const *filename) { #if LOL_USE_FFMPEG - if (!g_ready) + /* Third argument specifies format */ + avformat_alloc_output_context2(&m_avformat, nullptr, "gif", filename); + if (!m_avformat) + { + msg::debug("could not create output context"); + 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, ERROR_TO_STRING(ret)); + return false; + } + } + + int ret = avformat_write_header(m_avformat, nullptr); + if (ret < 0) { - g_ready = true; - av_register_all(); + msg::error("could not write header: %s\n", ERROR_TO_STRING(ret)); + return false; } #endif + + return true; } -Movie::~Movie() +bool movie::push_image(image &im) { #if LOL_USE_FFMPEG + // 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.size().x * im.size().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", ERROR_TO_STRING(ret)); + 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", ERROR_TO_STRING(ret)); + return false; + } + } #endif - delete m_data; + return true; +} + +void movie::close() +{ +#if LOL_USE_FFMPEG + // 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); +#endif } -void Movie::Feed(image const &image) +bool movie::open_codec() { #if LOL_USE_FFMPEG + 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", ERROR_TO_STRING(ret)); + return false; + } + + ret = avcodec_parameters_from_context(m_stream->codecpar, m_avcodec); + if (ret < 0) + { + msg::error("cannot copy stream parameters\n"); + return false; + } #endif + + return true; } -} /* namespace lol */ +} // namespace lol diff --git a/src/lol/image/movie.h b/src/lol/image/movie.h index b7e0609a..e188999e 100644 --- a/src/lol/image/movie.h +++ b/src/lol/image/movie.h @@ -1,7 +1,7 @@ // // Lol Engine // -// Copyright © 2010—2015 Sam Hocevar +// Copyright © 2010—2017 Sam Hocevar // // Lol Engine is free software. It comes without any warranty, to // the extent permitted by applicable law. You can redistribute it @@ -13,32 +13,41 @@ #pragma once // -// The Movie class +// The movie class // --------------- // -#include +#include +#include + +extern "C" struct AVFormatContext; +extern "C" struct AVCodecContext; +extern "C" struct AVStream; +extern "C" struct AVFrame; namespace lol { -class Movie +class movie { public: - Movie(String const &name, ivec2 size, float fps); + movie(ivec2 size); - /* TODO: Rule of three */ -#if 0 - Movie(Movie const &other); - Movie & operator =(Movie other); -#endif - ~Movie(); + bool open_file(char const *filename); + bool push_image(image &im); + void close(); - void Feed(image const &image); +private: + bool open_codec(); private: - class MovieData *m_data; + AVFormatContext *m_avformat; + AVCodecContext *m_avcodec; + AVStream *m_stream; + AVFrame *m_frame; + ivec2 m_size; + int m_index; }; -} /* namespace lol */ +} // namespace lol