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