| @@ -17,224 +17,21 @@ | |||||
| #include <lol/engine.h> | #include <lol/engine.h> | ||||
| #include "loldebug.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) | int main(int argc, char **argv) | ||||
| { | { | ||||
| UNUSED(argc, 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; | return EXIT_FAILURE; | ||||
| for (int i = 0; i < 256; ++i) | 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 y = 0; y < size.y; ++y) | ||||
| for (int x = 0; x < size.x; ++x) | for (int x = 0; x < size.x; ++x) | ||||
| { | { | ||||
| @@ -244,11 +41,11 @@ int main(int argc, char **argv) | |||||
| } | } | ||||
| im.unlock2d(data); | im.unlock2d(data); | ||||
| if (!enc.push_image(im)) | |||||
| if (!movie.push_image(im)) | |||||
| break; | break; | ||||
| } | } | ||||
| enc.close(); | |||||
| movie.close(); | |||||
| return 0; | return 0; | ||||
| } | } | ||||
| @@ -1,7 +1,7 @@ | |||||
| // | // | ||||
| // Lol Engine | // 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 | // Lol Engine is free software. It comes without any warranty, to | ||||
| // the extent permitted by applicable law. You can redistribute it | // the extent permitted by applicable law. You can redistribute it | ||||
| @@ -15,7 +15,13 @@ | |||||
| #if LOL_USE_FFMPEG | #if LOL_USE_FFMPEG | ||||
| extern "C" | extern "C" | ||||
| { | { | ||||
| # include <libavutil/avassert.h> | |||||
| # include <libavutil/channel_layout.h> | |||||
| # include <libavutil/mathematics.h> | |||||
| # include <libavutil/timestamp.h> | |||||
| # include <libavformat/avformat.h> | # include <libavformat/avformat.h> | ||||
| # include <libswscale/swscale.h> | |||||
| # include <libswresample/swresample.h> | |||||
| } | } | ||||
| #endif | #endif | ||||
| @@ -23,49 +29,194 @@ namespace lol | |||||
| { | { | ||||
| #if LOL_USE_FFMPEG | #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 | #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 | #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 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 | #endif | ||||
| return true; | |||||
| } | } | ||||
| Movie::~Movie() | |||||
| bool movie::push_image(image &im) | |||||
| { | { | ||||
| #if LOL_USE_FFMPEG | #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 | #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 | #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 | #endif | ||||
| return true; | |||||
| } | } | ||||
| } /* namespace lol */ | |||||
| } // namespace lol | |||||
| @@ -1,7 +1,7 @@ | |||||
| // | // | ||||
| // Lol Engine | // 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 | // Lol Engine is free software. It comes without any warranty, to | ||||
| // the extent permitted by applicable law. You can redistribute it | // the extent permitted by applicable law. You can redistribute it | ||||
| @@ -13,32 +13,41 @@ | |||||
| #pragma once | #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 | namespace lol | ||||
| { | { | ||||
| class Movie | |||||
| class movie | |||||
| { | { | ||||
| public: | 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: | 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 | |||||