@@ -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 | |||||