| @@ -17,224 +17,21 @@ | |||
| #include <lol/engine.h> | |||
| #include "loldebug.h" | |||
| extern "C" { | |||
| #include <libavutil/avassert.h> | |||
| #include <libavutil/channel_layout.h> | |||
| #include <libavutil/mathematics.h> | |||
| #include <libavutil/timestamp.h> | |||
| #include <libavformat/avformat.h> | |||
| #include <libswscale/swscale.h> | |||
| #include <libswresample/swresample.h> | |||
| } | |||
| 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<PixelFormat::RGB_8>(); | |||
| 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<u8vec3> &data = im.lock2d<PixelFormat::RGB_8>(); | |||
| lol::array2d<lol::u8vec3> &data = im.lock2d<lol::PixelFormat::RGB_8>(); | |||
| 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; | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| // | |||
| // Lol Engine | |||
| // | |||
| // Copyright © 2010—2016 Sam Hocevar <sam@hocevar.net> | |||
| // Copyright © 2010—2017 Sam Hocevar <sam@hocevar.net> | |||
| // | |||
| // 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 <libavutil/avassert.h> | |||
| # include <libavutil/channel_layout.h> | |||
| # include <libavutil/mathematics.h> | |||
| # include <libavutil/timestamp.h> | |||
| # include <libavformat/avformat.h> | |||
| # include <libswscale/swscale.h> | |||
| # include <libswresample/swresample.h> | |||
| } | |||
| #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<PixelFormat::RGB_8>(); | |||
| 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 | |||
| @@ -1,7 +1,7 @@ | |||
| // | |||
| // Lol Engine | |||
| // | |||
| // Copyright © 2010—2015 Sam Hocevar <sam@hocevar.net> | |||
| // Copyright © 2010—2017 Sam Hocevar <sam@hocevar.net> | |||
| // | |||
| // 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 <lol/image/movie.h> | |||
| #include <lol/image/pixel.h> | |||
| #include <lol/image/image.h> | |||
| 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 | |||