Feature: Switch from ImageMagick to FFmpeg for GIF generation

This commit is contained in:
Vicki Pfau 2019-09-17 19:06:58 -07:00
parent 8219b70c2e
commit 8708a0db52
14 changed files with 234 additions and 355 deletions

View File

@ -89,6 +89,7 @@ Misc:
- Qt: Show error message if file failed to load
- Qt: Scale pixel color values to full range (fixes mgba.io/i/1511)
- Qt, OpenGL: Disable integer scaling for dimensions that don't fit
- Feature: Switch from ImageMagick to FFmpeg for GIF generation
0.7.2: (2019-05-25)
Emulation fixes:

View File

@ -36,7 +36,6 @@ if(NOT LIBMGBA_ONLY)
set(USE_MINIZIP ON CACHE BOOL "Whether or not to enable external minizip support")
set(USE_PNG ON CACHE BOOL "Whether or not to enable PNG support")
set(USE_LIBZIP ON CACHE BOOL "Whether or not to enable LIBZIP support")
set(USE_MAGICK ON CACHE BOOL "Whether or not to enable ImageMagick support")
set(USE_SQLITE3 ON CACHE BOOL "Whether or not to enable SQLite3 support")
set(USE_ELF ON CACHE BOOL "Whether or not to enable ELF support")
set(M_CORE_GBA ON CACHE BOOL "Build Game Boy Advance core")
@ -462,12 +461,11 @@ set(WANT_PNG ${USE_PNG})
set(WANT_SQLITE3 ${USE_SQLITE3})
set(USE_CMOCKA ${BUILD_SUITE})
find_feature(USE_FFMPEG "libavcodec;libavformat;libavutil;libswscale")
find_feature(USE_FFMPEG "libavcodec;libavfilter;libavformat;libavutil;libswscale")
find_feature(USE_ZLIB "ZLIB")
find_feature(USE_MINIZIP "minizip")
find_feature(USE_PNG "PNG")
find_feature(USE_LIBZIP "libzip")
find_feature(USE_MAGICK "MagickWand")
find_feature(USE_EPOXY "epoxy")
find_feature(USE_CMOCKA "cmocka")
find_feature(USE_SQLITE3 "sqlite3")
@ -513,18 +511,20 @@ if(USE_FFMPEG)
list(APPEND FEATURES LIBAVRESAMPLE)
list(APPEND FEATURES LIBAV)
endif()
include_directories(AFTER ${LIBAVCODEC_INCLUDE_DIRS} ${LIBAVFORMAT_INCLUDE_DIRS} ${LIBAVRESAMPLE_INCLUDE_DIRS} ${LIBAVUTIL_INCLUDE_DIRS} ${LIBSWRESAMPLE_INCLUDE_DIRS} ${LIBSWSCALE_INCLUDE_DIRS})
link_directories(${LIBAVCODEC_LIBRARY_DIRS} ${LIBAVFORMAT_LIBRARY_DIRS} ${LIBAVRESAMPLE_LIBRARY_DIRS} ${LIBAVUTIL_LIBRARY_DIRS} ${LIBSWRESAMPLE_LIBRARY_DIRS} ${LIBSWSCALE_LIBRARY_DIRS})
include_directories(AFTER ${LIBAVCODEC_INCLUDE_DIRS} ${LIBAVFILTER_INCLUDE_DIRS} ${LIBAVFORMAT_INCLUDE_DIRS} ${LIBAVRESAMPLE_INCLUDE_DIRS} ${LIBAVUTIL_INCLUDE_DIRS} ${LIBSWRESAMPLE_INCLUDE_DIRS} ${LIBSWSCALE_INCLUDE_DIRS})
link_directories(${LIBAVCODEC_LIBRARY_DIRS} ${LIBAVFILTER_LIBRARY_DIRS} ${LIBAVFORMAT_LIBRARY_DIRS} ${LIBAVRESAMPLE_LIBRARY_DIRS} ${LIBAVUTIL_LIBRARY_DIRS} ${LIBSWRESAMPLE_LIBRARY_DIRS} ${LIBSWSCALE_LIBRARY_DIRS})
list(APPEND FEATURE_SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/feature/ffmpeg/ffmpeg-encoder.c")
string(REGEX MATCH "^[0-9]+" LIBAVCODEC_VERSION_MAJOR ${libavcodec_VERSION})
string(REGEX MATCH "^[0-9]+" LIBAVFILTER_VERSION_MAJOR ${libavfilter_VERSION})
string(REGEX MATCH "^[0-9]+" LIBAVFORMAT_VERSION_MAJOR ${libavformat_VERSION})
string(REGEX MATCH "^[0-9]+" LIBAVUTIL_VERSION_MAJOR ${libavutil_VERSION})
string(REGEX MATCH "^[0-9]+" LIBSWSCALE_VERSION_MAJOR ${libswscale_VERSION})
list(APPEND DEPENDENCY_LIB ${LIBAVCODEC_LIBRARIES} ${LIBAVFORMAT_LIBRARIES} ${LIBAVRESAMPLE_LIBRARIES} ${LIBAVUTIL_LIBRARIES} ${LIBSWSCALE_LIBRARIES} ${LIBSWRESAMPLE_LIBRARIES})
list(APPEND DEPENDENCY_LIB ${LIBAVCODEC_LIBRARIES} ${LIBAVFILTER_LIBRARIES} ${LIBAVFORMAT_LIBRARIES} ${LIBAVRESAMPLE_LIBRARIES} ${LIBAVUTIL_LIBRARIES} ${LIBSWSCALE_LIBRARIES} ${LIBSWRESAMPLE_LIBRARIES})
if(WIN32)
list(APPEND DEPENDENCY_LIB bcrypt)
endif()
set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libavcodec${LIBAVCODEC_VERSION_MAJOR}|libavcodec-extra-${LIBAVCODEC_VERSION_MAJOR}|libavcodec-ffmpeg${LIBAVCODEC_VERSION_MAJOR}|libavcodec-ffmpeg-extra${LIBAVCODEC_VERSION_MAJOR}")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libavfilter${LIBAVFILTER_VERSION_MAJOR}|libavfilter-ffmpeg${LIBAVFILTER_VERSION_MAJOR}")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libavformat${LIBAVFORMAT_VERSION_MAJOR}|libavformat-ffmpeg${LIBAVFORMAT_VERSION_MAJOR}")
if(USE_LIBSWRESAMPLE)
string(REGEX MATCH "^[0-9]+" LIBSWRESAMPLE_VERSION_MAJOR ${libswresample_VERSION})
@ -544,29 +544,6 @@ endif()
list(APPEND THIRD_PARTY_SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/third-party/blip_buf/blip_buf.c")
if(USE_MAGICK)
list(APPEND FEATURES MAGICK)
include_directories(AFTER ${MAGICKWAND_INCLUDE_DIRS})
link_directories(${MAGICKWAND_LIBRARY_DIRS})
list(APPEND FEATURE_SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/feature/imagemagick/imagemagick-gif-encoder.c")
list(APPEND DEPENDENCY_LIB ${MAGICKWAND_LIBRARIES})
string(REGEX MATCH "^[0-9]+\\.[0-9]+" MAGICKWAND_VERSION_PARTIAL ${MagickWand_VERSION})
string(REGEX MATCH "^[0-9]+" MAGICKWAND_VERSION_MAJOR ${MagickWand_VERSION})
if(${MAGICKWAND_VERSION_PARTIAL} STREQUAL "6.7")
set(MAGICKWAND_DEB_VERSION "5")
elseif(${MagickWand_VERSION} STREQUAL "6.9.10")
set(MAGICKWAND_DEB_VERSION "-6.q16-6")
elseif(${MagickWand_VERSION} STREQUAL "6.9.7")
set(MAGICKWAND_DEB_VERSION "-6.q16-3")
else()
set(MAGICKWAND_DEB_VERSION "-6.q16-2")
endif()
list(APPEND FEATURE_DEFINES MAGICKWAND_VERSION_MAJOR=${MAGICKWAND_VERSION_MAJOR})
list(APPEND FEATURE_FLAGS ${MAGICKWAND_CFLAGS_OTHER})
set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libmagickwand${MAGICKWAND_DEB_VERSION}")
endif()
if(WANT_ZLIB AND NOT USE_ZLIB)
set(SKIP_INSTALL_ALL ON)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/third-party/zlib zlib EXCLUDE_FROM_ALL)
@ -1240,8 +1217,7 @@ if(NOT QUIET AND NOT LIBMGBA_ONLY)
message(STATUS " CLI debugger: ${USE_EDITLINE}")
endif()
message(STATUS " GDB stub: ${USE_GDB_STUB}")
message(STATUS " Video recording: ${USE_FFMPEG}")
message(STATUS " GIF recording: ${USE_MAGICK}")
message(STATUS " GIF/Video recording: ${USE_FFMPEG}")
message(STATUS " Screenshot/advanced savestate support: ${USE_PNG}")
message(STATUS " ZIP support: ${SUMMARY_ZIP}")
message(STATUS " 7-Zip support: ${USE_LZMA}")

View File

@ -145,7 +145,7 @@ This will build and install mGBA into `/usr/bin` and `/usr/lib`. Dependencies th
If you are on macOS, the steps are a little different. Assuming you are using the homebrew package manager, the recommended commands to obtain the dependencies and build are:
brew install cmake ffmpeg imagemagick libzip qt5 sdl2 libedit pkg-config
brew install cmake ffmpeg libzip qt5 sdl2 libedit pkg-config
mkdir build
cd build
cmake -DCMAKE_PREFIX_PATH=`brew --prefix qt5` ..
@ -159,11 +159,11 @@ To build on Windows for development, using MSYS2 is recommended. Follow the inst
For x86 (32 bit) builds:
pacman -Sy --needed base-devel git mingw-w64-i686-{cmake,ffmpeg,gcc,gdb,imagemagick,libelf,libepoxy,libzip,pkg-config,qt5,SDL2,ntldd-git}
pacman -Sy --needed base-devel git mingw-w64-i686-{cmake,ffmpeg,gcc,gdb,libelf,libepoxy,libzip,pkg-config,qt5,SDL2,ntldd-git}
For x86_64 (64 bit) builds:
pacman -Sy --needed base-devel git mingw-w64-x86_64-{cmake,ffmpeg,gcc,gdb,imagemagick,libelf,libepoxy,libzip,pkg-config,qt5,SDL2,ntldd-git}
pacman -Sy --needed base-devel git mingw-w64-x86_64-{cmake,ffmpeg,gcc,gdb,libelf,libepoxy,libzip,pkg-config,qt5,SDL2,ntldd-git}
Check out the source code by running this command:

View File

@ -95,10 +95,6 @@
#cmakedefine USE_LZMA
#endif
#ifndef USE_MAGICK
#cmakedefine USE_MAGICK
#endif
#ifndef USE_MINIZIP
#cmakedefine USE_MINIZIP
#endif

View File

@ -11,6 +11,9 @@
#include <libavcodec/version.h>
#include <libavcodec/avcodec.h>
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h>
#include <libavutil/version.h>
#if LIBAVUTIL_VERSION_MAJOR >= 53
#include <libavutil/buffer.h>
@ -38,7 +41,9 @@ enum {
};
void FFmpegEncoderInit(struct FFmpegEncoder* encoder) {
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100)
av_register_all();
#endif
encoder->d.videoDimensionsChanged = _ffmpegSetVideoDimensions;
encoder->d.postVideoFrame = _ffmpegPostVideoFrame;
@ -49,11 +54,27 @@ void FFmpegEncoderInit(struct FFmpegEncoder* encoder) {
encoder->videoCodec = NULL;
encoder->containerFormat = NULL;
FFmpegEncoderSetAudio(encoder, "flac", 0);
FFmpegEncoderSetVideo(encoder, "png", 0);
FFmpegEncoderSetVideo(encoder, "libx264", 0, 0);
FFmpegEncoderSetContainer(encoder, "matroska");
FFmpegEncoderSetDimensions(encoder, GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS);
encoder->iwidth = GBA_VIDEO_HORIZONTAL_PIXELS;
encoder->iheight = GBA_VIDEO_VERTICAL_PIXELS;
encoder->frameskip = 1;
encoder->skipResidue = 0;
encoder->ipixFormat =
#ifdef COLOR_16_BIT
#ifdef COLOR_5_6_5
AV_PIX_FMT_RGB565;
#else
AV_PIX_FMT_BGR555;
#endif
#else
#ifndef USE_LIBAV
AV_PIX_FMT_0BGR32;
#else
AV_PIX_FMT_BGR32;
#endif
#endif
encoder->resampleContext = NULL;
encoder->absf = NULL;
encoder->context = NULL;
@ -66,6 +87,15 @@ void FFmpegEncoderInit(struct FFmpegEncoder* encoder) {
encoder->video = NULL;
encoder->videoStream = NULL;
encoder->videoFrame = NULL;
encoder->graph = NULL;
encoder->source = NULL;
encoder->sink = NULL;
encoder->sinkFrame = NULL;
int i;
for (i = 0; i < FFMPEG_FILTERS_MAX; ++i) {
encoder->filters[i] = NULL;
}
}
bool FFmpegEncoderSetAudio(struct FFmpegEncoder* encoder, const char* acodec, unsigned abr) {
@ -130,7 +160,7 @@ bool FFmpegEncoderSetAudio(struct FFmpegEncoder* encoder, const char* acodec, un
return true;
}
bool FFmpegEncoderSetVideo(struct FFmpegEncoder* encoder, const char* vcodec, unsigned vbr) {
bool FFmpegEncoderSetVideo(struct FFmpegEncoder* encoder, const char* vcodec, unsigned vbr, int frameskip) {
static const struct {
enum AVPixelFormat format;
int priority;
@ -149,7 +179,8 @@ bool FFmpegEncoderSetVideo(struct FFmpegEncoder* encoder, const char* vcodec, un
#endif
{ AV_PIX_FMT_YUV422P, 4 },
{ AV_PIX_FMT_YUV444P, 5 },
{ AV_PIX_FMT_YUV420P, 6 }
{ AV_PIX_FMT_YUV420P, 6 },
{ AV_PIX_FMT_PAL8, 7 },
};
if (!vcodec) {
@ -179,6 +210,7 @@ bool FFmpegEncoderSetVideo(struct FFmpegEncoder* encoder, const char* vcodec, un
}
encoder->videoCodec = vcodec;
encoder->videoBitrate = vbr;
encoder->frameskip = frameskip + 1;
return true;
}
@ -226,6 +258,7 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
encoder->currentAudioSample = 0;
encoder->currentAudioFrame = 0;
encoder->currentVideoFrame = 0;
encoder->skipResidue = 0;
AVOutputFormat* oformat = av_guess_format(encoder->containerFormat, 0, 0);
#ifndef USE_LIBAV
@ -325,8 +358,8 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
encoder->video->bit_rate = encoder->videoBitrate;
encoder->video->width = encoder->width;
encoder->video->height = encoder->height;
encoder->video->time_base = (AVRational) { VIDEO_TOTAL_LENGTH, GBA_ARM7TDMI_FREQUENCY };
encoder->video->framerate = (AVRational) { GBA_ARM7TDMI_FREQUENCY, VIDEO_TOTAL_LENGTH };
encoder->video->time_base = (AVRational) { VIDEO_TOTAL_LENGTH * encoder->frameskip, GBA_ARM7TDMI_FREQUENCY };
encoder->video->framerate = (AVRational) { GBA_ARM7TDMI_FREQUENCY, VIDEO_TOTAL_LENGTH * encoder->frameskip };
encoder->video->pix_fmt = encoder->pixFormat;
encoder->video->gop_size = 60;
encoder->video->max_b_frames = 3;
@ -365,6 +398,54 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
encoder->video->pix_fmt = AV_PIX_FMT_YUV444P;
}
if (encoder->pixFormat == AV_PIX_FMT_PAL8) {
encoder->graph = avfilter_graph_alloc();
const struct AVFilter* source = avfilter_get_by_name("buffer");
const struct AVFilter* sink = avfilter_get_by_name("buffersink");
const struct AVFilter* split = avfilter_get_by_name("split");
const struct AVFilter* palettegen = avfilter_get_by_name("palettegen");
const struct AVFilter* paletteuse = avfilter_get_by_name("paletteuse");
if (!source || !sink || !split || !palettegen || !paletteuse || !encoder->graph) {
FFmpegEncoderClose(encoder);
return false;
}
char args[256];
snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d",
encoder->video->width, encoder->video->height, encoder->ipixFormat,
encoder->video->time_base.num, encoder->video->time_base.den);
int res = 0;
res |= avfilter_graph_create_filter(&encoder->source, source, NULL, args, NULL, encoder->graph);
res |= avfilter_graph_create_filter(&encoder->sink, sink, NULL, NULL, NULL, encoder->graph);
res |= avfilter_graph_create_filter(&encoder->filters[0], split, NULL, NULL, NULL, encoder->graph);
res |= avfilter_graph_create_filter(&encoder->filters[1], palettegen, NULL, "reserve_transparent=off", NULL, encoder->graph);
res |= avfilter_graph_create_filter(&encoder->filters[2], paletteuse, NULL, "dither=none", NULL, encoder->graph);
if (res < 0) {
FFmpegEncoderClose(encoder);
return false;
}
res = 0;
res |= avfilter_link(encoder->source, 0, encoder->filters[0], 0);
res |= avfilter_link(encoder->filters[0], 0, encoder->filters[1], 0);
res |= avfilter_link(encoder->filters[0], 1, encoder->filters[2], 0);
res |= avfilter_link(encoder->filters[1], 0, encoder->filters[2], 1);
res |= avfilter_link(encoder->filters[2], 0, encoder->sink, 0);
if (res < 0 || avfilter_graph_config(encoder->graph, NULL) < 0) {
FFmpegEncoderClose(encoder);
return false;
}
#if LIBAVCODEC_VERSION_MAJOR >= 55
encoder->sinkFrame = av_frame_alloc();
#else
encoder->sinkFrame = avcodec_alloc_frame();
#endif
}
if (avcodec_open2(encoder->video, vcodec, 0) < 0) {
FFmpegEncoderClose(encoder);
return false;
@ -374,12 +455,12 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
#else
encoder->videoFrame = avcodec_alloc_frame();
#endif
encoder->videoFrame->format = encoder->video->pix_fmt;
encoder->videoFrame->format = encoder->video->pix_fmt != AV_PIX_FMT_PAL8 ? encoder->video->pix_fmt : encoder->ipixFormat;
encoder->videoFrame->width = encoder->video->width;
encoder->videoFrame->height = encoder->video->height;
encoder->videoFrame->pts = 0;
_ffmpegSetVideoDimensions(&encoder->d, encoder->iwidth, encoder->iheight);
av_image_alloc(encoder->videoFrame->data, encoder->videoFrame->linesize, encoder->video->width, encoder->video->height, encoder->video->pix_fmt, 32);
av_image_alloc(encoder->videoFrame->data, encoder->videoFrame->linesize, encoder->videoFrame->width, encoder->videoFrame->height, encoder->videoFrame->format, 32);
#ifdef FFMPEG_USE_CODECPAR
avcodec_parameters_from_context(encoder->videoStream->codecpar, encoder->video);
#endif
@ -401,6 +482,18 @@ void FFmpegEncoderClose(struct FFmpegEncoder* encoder) {
}
}
if (encoder->video) {
if (encoder->graph) {
if (av_buffersrc_add_frame(encoder->source, NULL) >= 0) {
while (true) {
int res = av_buffersink_get_frame(encoder->sink, encoder->sinkFrame);
if (res < 0) {
break;
}
_ffmpegWriteVideoFrame(encoder, encoder->sinkFrame);
av_frame_unref(encoder->sinkFrame);
}
}
}
while (true) {
if (!_ffmpegWriteVideoFrame(encoder, NULL)) {
break;
@ -460,6 +553,15 @@ void FFmpegEncoderClose(struct FFmpegEncoder* encoder) {
#endif
}
if (encoder->sinkFrame) {
#if LIBAVCODEC_VERSION_MAJOR >= 55
av_frame_free(&encoder->sinkFrame);
#else
avcodec_free_frame(&encoder->sinkFrame);
#endif
encoder->sinkFrame = NULL;
}
if (encoder->video) {
avcodec_close(encoder->video);
encoder->video = NULL;
@ -470,6 +572,18 @@ void FFmpegEncoderClose(struct FFmpegEncoder* encoder) {
encoder->scaleContext = NULL;
}
if (encoder->graph) {
avfilter_graph_free(&encoder->graph);
encoder->graph = NULL;
encoder->source = NULL;
encoder->sink = NULL;
int i;
for (i = 0; i < FFMPEG_FILTERS_MAX; ++i) {
encoder->filters[i] = NULL;
}
}
if (encoder->context) {
avformat_free_context(encoder->context);
encoder->context = NULL;
@ -596,6 +710,10 @@ void _ffmpegPostVideoFrame(struct mAVStream* stream, const color_t* pixels, size
if (!encoder->context || !encoder->videoCodec) {
return;
}
encoder->skipResidue = (encoder->skipResidue + 1) % encoder->frameskip;
if (encoder->skipResidue) {
return;
}
stride *= BYTES_PER_PIXEL;
#if LIBAVCODEC_VERSION_MAJOR >= 55
@ -606,7 +724,21 @@ void _ffmpegPostVideoFrame(struct mAVStream* stream, const color_t* pixels, size
sws_scale(encoder->scaleContext, (const uint8_t* const*) &pixels, (const int*) &stride, 0, encoder->iheight, encoder->videoFrame->data, encoder->videoFrame->linesize);
if (encoder->graph) {
if (av_buffersrc_add_frame(encoder->source, encoder->videoFrame) < 0) {
return;
}
while (true) {
int res = av_buffersink_get_frame(encoder->sink, encoder->sinkFrame);
if (res < 0) {
break;
}
_ffmpegWriteVideoFrame(encoder, encoder->sinkFrame);
av_frame_unref(encoder->sinkFrame);
}
} else {
_ffmpegWriteVideoFrame(encoder, encoder->videoFrame);
}
}
bool _ffmpegWriteVideoFrame(struct FFmpegEncoder* encoder, struct AVFrame* videoFrame) {
@ -651,20 +783,7 @@ static void _ffmpegSetVideoDimensions(struct mAVStream* stream, unsigned width,
if (encoder->scaleContext) {
sws_freeContext(encoder->scaleContext);
}
encoder->scaleContext = sws_getContext(encoder->iwidth, encoder->iheight,
#ifdef COLOR_16_BIT
#ifdef COLOR_5_6_5
AV_PIX_FMT_RGB565,
#else
AV_PIX_FMT_BGR555,
#endif
#else
#ifndef USE_LIBAV
AV_PIX_FMT_0BGR32,
#else
AV_PIX_FMT_BGR32,
#endif
#endif
encoder->videoFrame->width, encoder->videoFrame->height, encoder->video->pix_fmt,
encoder->scaleContext = sws_getContext(encoder->iwidth, encoder->iheight, encoder->ipixFormat,
encoder->videoFrame->width, encoder->videoFrame->height, encoder->videoFrame->format,
SWS_POINT, 0, 0, 0);
}

View File

@ -34,6 +34,8 @@ CXX_GUARD_START
#define FFMPEG_USE_PACKET_UNREF
#endif
#define FFMPEG_FILTERS_MAX 4
struct FFmpegEncoder {
struct mAVStream d;
struct AVFormatContext* context;
@ -70,19 +72,28 @@ struct FFmpegEncoder {
struct AVCodecContext* video;
enum AVPixelFormat pixFormat;
enum AVPixelFormat ipixFormat;
struct AVFrame* videoFrame;
int width;
int height;
int iwidth;
int iheight;
int frameskip;
int skipResidue;
int64_t currentVideoFrame;
struct SwsContext* scaleContext;
struct AVStream* videoStream;
struct AVFilterGraph* graph;
struct AVFilterContext* source;
struct AVFilterContext* sink;
struct AVFilterContext* filters[FFMPEG_FILTERS_MAX];
struct AVFrame* sinkFrame;
};
void FFmpegEncoderInit(struct FFmpegEncoder*);
bool FFmpegEncoderSetAudio(struct FFmpegEncoder*, const char* acodec, unsigned abr);
bool FFmpegEncoderSetVideo(struct FFmpegEncoder*, const char* vcodec, unsigned vbr);
bool FFmpegEncoderSetVideo(struct FFmpegEncoder*, const char* vcodec, unsigned vbr, int frameskip);
bool FFmpegEncoderSetContainer(struct FFmpegEncoder*, const char* container);
void FFmpegEncoderSetDimensions(struct FFmpegEncoder*, int width, int height);
bool FFmpegEncoderVerifyContainer(struct FFmpegEncoder*);

View File

@ -1,107 +0,0 @@
/* Copyright (c) 2013-2015 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "imagemagick-gif-encoder.h"
#include <mgba/internal/gba/gba.h>
#include <mgba/gba/interface.h>
#include <mgba-util/string.h>
static void _magickPostVideoFrame(struct mAVStream*, const color_t* pixels, size_t stride);
static void _magickVideoDimensionsChanged(struct mAVStream*, unsigned width, unsigned height);
void ImageMagickGIFEncoderInit(struct ImageMagickGIFEncoder* encoder) {
encoder->wand = 0;
encoder->d.videoDimensionsChanged = _magickVideoDimensionsChanged;
encoder->d.postVideoFrame = _magickPostVideoFrame;
encoder->d.postAudioFrame = 0;
encoder->d.postAudioBuffer = 0;
encoder->frameskip = 2;
encoder->delayMs = -1;
encoder->iwidth = GBA_VIDEO_HORIZONTAL_PIXELS;
encoder->iheight = GBA_VIDEO_VERTICAL_PIXELS;
}
void ImageMagickGIFEncoderSetParams(struct ImageMagickGIFEncoder* encoder, int frameskip, int delayMs) {
if (ImageMagickGIFEncoderIsOpen(encoder)) {
return;
}
encoder->frameskip = frameskip;
encoder->delayMs = delayMs;
}
bool ImageMagickGIFEncoderOpen(struct ImageMagickGIFEncoder* encoder, const char* outfile) {
MagickWandGenesis();
encoder->wand = NewMagickWand();
MagickSetImageFormat(encoder->wand, "GIF");
MagickSetImageDispose(encoder->wand, PreviousDispose);
encoder->outfile = strdup(outfile);
encoder->frame = malloc(encoder->iwidth * encoder->iheight * 4);
encoder->currentFrame = 0;
return true;
}
bool ImageMagickGIFEncoderClose(struct ImageMagickGIFEncoder* encoder) {
if (!encoder->wand) {
return false;
}
MagickBooleanType success = MagickWriteImages(encoder->wand, encoder->outfile, MagickTrue);
DestroyMagickWand(encoder->wand);
encoder->wand = 0;
free(encoder->outfile);
free(encoder->frame);
MagickWandTerminus();
return success == MagickTrue;
}
bool ImageMagickGIFEncoderIsOpen(struct ImageMagickGIFEncoder* encoder) {
return !!encoder->wand;
}
static void _magickPostVideoFrame(struct mAVStream* stream, const color_t* pixels, size_t stride) {
struct ImageMagickGIFEncoder* encoder = (struct ImageMagickGIFEncoder*) stream;
if (encoder->currentFrame % (encoder->frameskip + 1)) {
++encoder->currentFrame;
return;
}
const uint8_t* p8 = (const uint8_t*) pixels;
size_t row;
for (row = 0; row < encoder->iheight; ++row) {
memcpy(&encoder->frame[row * encoder->iwidth], &p8[row * 4 * stride], encoder->iwidth * 4);
}
MagickConstituteImage(encoder->wand, encoder->iwidth, encoder->iheight, "RGBP", CharPixel, encoder->frame);
uint64_t ts = encoder->currentFrame;
uint64_t nts = encoder->currentFrame + encoder->frameskip + 1;
if (encoder->delayMs >= 0) {
ts *= encoder->delayMs;
nts *= encoder->delayMs;
ts /= 10;
nts /= 10;
} else {
ts *= VIDEO_TOTAL_LENGTH * 100;
nts *= VIDEO_TOTAL_LENGTH * 100;
ts /= GBA_ARM7TDMI_FREQUENCY;
nts /= GBA_ARM7TDMI_FREQUENCY;
}
MagickSetImageDelay(encoder->wand, nts - ts);
++encoder->currentFrame;
}
static void _magickVideoDimensionsChanged(struct mAVStream* stream, unsigned width, unsigned height) {
struct ImageMagickGIFEncoder* encoder = (struct ImageMagickGIFEncoder*) stream;
if (width * height > encoder->iwidth * encoder->iheight) {
free(encoder->frame);
encoder->frame = malloc(width * height * 4);
}
encoder->iwidth = width;
encoder->iheight = height;
}

View File

@ -1,43 +0,0 @@
/* Copyright (c) 2013-2015 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef IMAGEMAGICK_GIF_ENCODER
#define IMAGEMAGICK_GIF_ENCODER
#include <mgba-util/common.h>
CXX_GUARD_START
#include <mgba/core/interface.h>
#if MAGICKWAND_VERSION_MAJOR >= 7
#include <MagickWand/MagickWand.h>
#else
#include <wand/MagickWand.h>
#endif
struct ImageMagickGIFEncoder {
struct mAVStream d;
MagickWand* wand;
char* outfile;
uint32_t* frame;
unsigned currentFrame;
int frameskip;
int delayMs;
unsigned iwidth;
unsigned iheight;
};
void ImageMagickGIFEncoderInit(struct ImageMagickGIFEncoder*);
void ImageMagickGIFEncoderSetParams(struct ImageMagickGIFEncoder* encoder, int frameskip, int delayMs);
bool ImageMagickGIFEncoderOpen(struct ImageMagickGIFEncoder*, const char* outfile);
bool ImageMagickGIFEncoderClose(struct ImageMagickGIFEncoder*);
bool ImageMagickGIFEncoderIsOpen(struct ImageMagickGIFEncoder*);
CXX_GUARD_END
#endif

View File

@ -5,7 +5,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "GIFView.h"
#ifdef USE_MAGICK
#ifdef USE_FFMPEG
#include "CoreController.h"
#include "GBAApp.h"
@ -13,9 +13,6 @@
#include <QMap>
#include <mgba/internal/gba/gba.h>
#include <mgba/internal/gba/video.h>
using namespace QGBA;
GIFView::GIFView(QWidget* parent)
@ -29,11 +26,9 @@ GIFView::GIFView(QWidget* parent)
connect(m_ui.selectFile, &QAbstractButton::clicked, this, &GIFView::selectFile);
connect(m_ui.filename, &QLineEdit::textChanged, this, &GIFView::setFilename);
connect(m_ui.frameskip, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
this, &GIFView::updateDelay);
connect(m_ui.delayAuto, &QAbstractButton::clicked, this, &GIFView::updateDelay);
ImageMagickGIFEncoderInit(&m_encoder);
FFmpegEncoderInit(&m_encoder);
FFmpegEncoderSetAudio(&m_encoder, nullptr, 0);
FFmpegEncoderSetContainer(&m_encoder, "gif");
}
GIFView::~GIFView() {
@ -44,34 +39,35 @@ void GIFView::setController(std::shared_ptr<CoreController> controller) {
connect(controller.get(), &CoreController::stopping, this, &GIFView::stopRecording);
connect(this, &GIFView::recordingStarted, controller.get(), &CoreController::setAVStream);
connect(this, &GIFView::recordingStopped, controller.get(), &CoreController::clearAVStream, Qt::DirectConnection);
QSize size(controller->screenDimensions());
FFmpegEncoderSetDimensions(&m_encoder, size.width(), size.height());
}
void GIFView::startRecording() {
int delayMs = m_ui.delayAuto->isChecked() ? -1 : m_ui.delayMs->value();
ImageMagickGIFEncoderSetParams(&m_encoder, m_ui.frameskip->value(), delayMs);
if (!ImageMagickGIFEncoderOpen(&m_encoder, m_filename.toUtf8().constData())) {
FFmpegEncoderSetVideo(&m_encoder, "gif", 0, m_ui.frameskip->value());
if (!FFmpegEncoderOpen(&m_encoder, m_filename.toUtf8().constData())) {
LOG(QT, ERROR) << tr("Failed to open output GIF file: %1").arg(m_filename);
return;
}
m_ui.start->setEnabled(false);
m_ui.stop->setEnabled(true);
m_ui.groupBox->setEnabled(false);
m_ui.frameskip->setEnabled(false);
emit recordingStarted(&m_encoder.d);
}
void GIFView::stopRecording() {
emit recordingStopped();
ImageMagickGIFEncoderClose(&m_encoder);
FFmpegEncoderClose(&m_encoder);
m_ui.stop->setEnabled(false);
m_ui.start->setEnabled(true);
m_ui.groupBox->setEnabled(true);
m_ui.frameskip->setEnabled(true);
}
void GIFView::selectFile() {
QString filename = GBAApp::app()->getSaveFileName(this, tr("Select output file"), tr("Graphics Interchange Format (*.gif)"));
if (!filename.isEmpty()) {
m_ui.filename->setText(filename);
if (!ImageMagickGIFEncoderIsOpen(&m_encoder)) {
if (!FFmpegEncoderIsOpen(&m_encoder)) {
m_ui.start->setEnabled(true);
}
}
@ -81,15 +77,4 @@ void GIFView::setFilename(const QString& fname) {
m_filename = fname;
}
void GIFView::updateDelay() {
if (!m_ui.delayAuto->isChecked()) {
return;
}
uint64_t s = (m_ui.frameskip->value() + 1);
s *= VIDEO_TOTAL_LENGTH * 1000;
s /= GBA_ARM7TDMI_FREQUENCY;
m_ui.delayMs->setValue(s);
}
#endif

View File

@ -5,7 +5,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#ifdef USE_MAGICK
#ifdef USE_FFMPEG
#include <QWidget>
@ -13,7 +13,7 @@
#include "ui_GIFView.h"
#include "feature/imagemagick/imagemagick-gif-encoder.h"
#include "feature/ffmpeg/ffmpeg-encoder.h"
namespace QGBA {
@ -41,12 +41,11 @@ signals:
private slots:
void selectFile();
void setFilename(const QString&);
void updateDelay();
private:
Ui::GIFView m_ui;
ImageMagickGIFEncoder m_encoder;
FFmpegEncoder m_encoder;
QString m_filename;
};

View File

@ -6,18 +6,52 @@
<rect>
<x>0</x>
<y>0</y>
<width>278</width>
<height>247</height>
<width>392</width>
<height>220</height>
</rect>
</property>
<property name="windowTitle">
<string>Record GIF</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<layout class="QGridLayout" name="gridLayout_3">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<item row="1" column="0">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label">
<property name="text">
<string>Frameskip</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QSpinBox" name="frameskip">
<property name="value">
<number>2</number>
</property>
</widget>
</item>
<item row="2" column="0" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QPushButton" name="start">
@ -51,6 +85,19 @@
</property>
</widget>
</item>
<item row="1" column="2">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="selectFile">
<property name="sizePolicy">
@ -74,81 +121,8 @@
</property>
</widget>
</item>
<item row="1" column="2">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string/>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Frameskip</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="frameskip">
<property name="value">
<number>2</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Frame delay (ms)</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="delayAuto">
<property name="text">
<string>Automatic</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="delayMs">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximum">
<number>5000</number>
</property>
<property name="value">
<number>50</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
@ -169,21 +143,5 @@
</hint>
</hints>
</connection>
<connection>
<sender>delayAuto</sender>
<signal>clicked(bool)</signal>
<receiver>delayMs</receiver>
<slot>setDisabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>202</x>
<y>177</y>
</hint>
<hint type="destinationlabel">
<x>192</x>
<y>148</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -280,7 +280,7 @@ void VideoView::setVideoCodec(const QString& codec, bool manual) {
} else {
m_videoCodecCstr = strdup(m_videoCodec.toUtf8().constData());
}
if (!FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr)) {
if (!FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr, 0)) {
free(m_videoCodecCstr);
m_videoCodecCstr = nullptr;
m_videoCodec = QString();
@ -317,7 +317,7 @@ void VideoView::setAudioBitrate(int br, bool manual) {
void VideoView::setVideoBitrate(int br, bool manual) {
m_vbr = br >= 0 ? br * 1000 : 0;
FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr);
FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr, 0);
validateSettings();
if (manual) {
uncheckIncompatible();

View File

@ -165,9 +165,6 @@ Window::~Window() {
#ifdef USE_FFMPEG
delete m_videoView;
#endif
#ifdef USE_MAGICK
delete m_gifView;
#endif
@ -507,9 +504,7 @@ void Window::openVideoWindow() {
}
m_videoView->show();
}
#endif
#ifdef USE_MAGICK
void Window::openGIFWindow() {
if (!m_gifView) {
m_gifView = new GIFView();
@ -1442,9 +1437,6 @@ void Window::setupMenu(QMenuBar* menubar) {
#ifdef USE_FFMPEG
addGameAction(tr("Record A/V..."), "recordOutput", this, &Window::openVideoWindow, "av");
#endif
#ifdef USE_MAGICK
addGameAction(tr("Record GIF..."), "recordGIF", this, &Window::openGIFWindow, "av");
#endif
@ -1874,13 +1866,11 @@ void Window::setController(CoreController* controller, const QString& fname) {
}
#endif
#ifdef USE_MAGICK
#ifdef USE_FFMPEG
if (m_gifView) {
m_gifView->setController(m_controller);
}
#endif
#ifdef USE_FFMPEG
if (m_videoView) {
m_videoView->setController(m_controller);
}

View File

@ -101,9 +101,6 @@ public slots:
#ifdef USE_FFMPEG
void openVideoWindow();
#endif
#ifdef USE_MAGICK
void openGIFWindow();
#endif
@ -224,9 +221,6 @@ private:
#ifdef USE_FFMPEG
VideoView* m_videoView = nullptr;
#endif
#ifdef USE_MAGICK
GIFView* m_gifView = nullptr;
#endif