Browse Source

movie: replace the old Movie class with a GIF encoder.

legacy
Sam Hocevar 7 years ago
parent
commit
c6374c7e07
3 changed files with 199 additions and 242 deletions
  1. +7
    -210
      doc/tutorial/16_movie.cpp
  2. +169
    -18
      src/image/movie.cpp
  3. +23
    -14
      src/lol/image/movie.h

+ 7
- 210
doc/tutorial/16_movie.cpp View File

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


+ 169
- 18
src/image/movie.cpp View File

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


+ 23
- 14
src/lol/image/movie.h View File

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


Loading…
Cancel
Save