Feature: Switch from ImageMagick to FFmpeg for GIF generation
@@ -89,6 +89,7 @@ - Switch: Support file associations
- 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:
@@ -36,7 +36,6 @@ set(USE_ZLIB ON CACHE BOOL "Whether or not to enable zlib support")
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 @@ else()
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})@@ -543,29 +543,6 @@ endif()
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)@@ -1240,8 +1217,7 @@ if(NOT WIN32)
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}")
@@ -145,7 +145,7 @@ This will build and install mGBA into `/usr/bin` and `/usr/lib`. Dependencies that are installed will be automatically detected, and features that are disabled if the dependencies are not found will be shown after running the `cmake` command after warnings about being unable to find them.
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 installation steps found on their [website](https://msys2.github.io). Make sure you're running the 32-bit version ("MSYS2 MinGW 32-bit") (or the 64-bit version "MSYS2 MinGW 64-bit" if you want to build for x86_64) and run this additional command (including the braces) to install the needed dependencies (please note that this involves downloading over 1100MiB of packages, so it will take a long time):
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:
@@ -95,10 +95,6 @@ #ifndef USE_LZMA
#cmakedefine USE_LZMA #endif -#ifndef USE_MAGICK -#cmakedefine USE_MAGICK -#endif - #ifndef USE_MINIZIP #cmakedefine USE_MINIZIP #endif
@@ -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 @@ PREFERRED_SAMPLE_RATE = 0x8000
}; 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 @@ encoder->audioCodec = NULL;
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 @@ encoder->postaudioBuffer = NULL;
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 @@ encoder->audioBitrate = abr;
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 @@ { AV_PIX_FMT_0RGB, 3 },
#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 @@ return false;
} encoder->videoCodec = vcodec; encoder->videoBitrate = vbr; + encoder->frameskip = frameskip + 1; return true; }@@ -226,6 +258,7 @@
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 @@ #endif
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 @@ av_opt_set(encoder->video->priv_data, "lossless", "1", 0);
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 @@ encoder->videoFrame = av_frame_alloc();
#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 @@ }
} } 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 @@ avcodec_free_frame(&encoder->videoFrame);
#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 @@ sws_freeContext(encoder->scaleContext);
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 @@ struct FFmpegEncoder* encoder = (struct FFmpegEncoder*) stream;
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 @@ ++encoder->currentVideoFrame;
sws_scale(encoder->scaleContext, (const uint8_t* const*) &pixels, (const int*) &stride, 0, encoder->iheight, encoder->videoFrame->data, encoder->videoFrame->linesize); - _ffmpegWriteVideoFrame(encoder, encoder->videoFrame); + 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 @@ encoder->iheight = height;
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); }
@@ -34,6 +34,8 @@ #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 8, 0)
#define FFMPEG_USE_PACKET_UNREF #endif +#define FFMPEG_FILTERS_MAX 4 + struct FFmpegEncoder { struct mAVStream d; struct AVFormatContext* context;@@ -70,19 +72,28 @@ struct AVStream* audioStream;
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*);
@@ -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; -}
@@ -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
@@ -5,16 +5,13 @@ * 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 "GIFView.h" -#ifdef USE_MAGICK +#ifdef USE_FFMPEG #include "CoreController.h" #include "GBAApp.h" #include "LogController.h" #include <QMap> - -#include <mgba/internal/gba/gba.h> -#include <mgba/internal/gba/video.h> using namespace QGBA;@@ -29,11 +26,9 @@
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); } }@@ -79,17 +75,6 @@ }
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
@@ -5,7 +5,7 @@ * 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/. */ #pragma once -#ifdef USE_MAGICK +#ifdef USE_FFMPEG #include <QWidget>@@ -13,7 +13,7 @@ #include <memory>
#include "ui_GIFView.h" -#include "feature/imagemagick/imagemagick-gif-encoder.h" +#include "feature/ffmpeg/ffmpeg-encoder.h" namespace QGBA {@@ -41,12 +41,11 @@
private slots: void selectFile(); void setFilename(const QString&); - void updateDelay(); private: Ui::GIFView m_ui; - ImageMagickGIFEncoder m_encoder; + FFmpegEncoder m_encoder; QString m_filename; };
@@ -6,18 +6,52 @@ <property name="geometry">
<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 @@ <string>Stop</string>
</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 @@ </sizepolicy>
</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/>@@ -166,22 +140,6 @@ </hint>
<hint type="destinationlabel"> <x>138</x> <y>123</y> - </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>
@@ -280,7 +280,7 @@ m_videoCodecCstr = nullptr;
} 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::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();
@@ -165,9 +165,6 @@ delete m_logView;
#ifdef USE_FFMPEG delete m_videoView; -#endif - -#ifdef USE_MAGICK delete m_gifView; #endif@@ -507,9 +504,7 @@ connect(this, &Window::shutdown, m_videoView, &QWidget::close);
} m_videoView->show(); } -#endif -#ifdef USE_MAGICK void Window::openGIFWindow() { if (!m_gifView) { m_gifView = new GIFView();@@ -1442,9 +1437,6 @@ #endif
#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 @@ m_console->setController(m_controller);
} #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); }
@@ -101,9 +101,6 @@ #endif
#ifdef USE_FFMPEG void openVideoWindow(); -#endif - -#ifdef USE_MAGICK void openGIFWindow(); #endif@@ -224,9 +221,6 @@ FrameView* m_frameView = nullptr;
#ifdef USE_FFMPEG VideoView* m_videoView = nullptr; -#endif - -#ifdef USE_MAGICK GIFView* m_gifView = nullptr; #endif