diff --git a/CHANGES b/CHANGES index d86113270..37e530d82 100644 --- a/CHANGES +++ b/CHANGES @@ -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: diff --git a/CMakeLists.txt b/CMakeLists.txt index edd2c4b66..14aff08a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}") diff --git a/README.md b/README.md index 111eaa337..f85550c84 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/src/core/flags.h.in b/src/core/flags.h.in index facd8743a..f214c7a8f 100644 --- a/src/core/flags.h.in +++ b/src/core/flags.h.in @@ -95,10 +95,6 @@ #cmakedefine USE_LZMA #endif -#ifndef USE_MAGICK -#cmakedefine USE_MAGICK -#endif - #ifndef USE_MINIZIP #cmakedefine USE_MINIZIP #endif diff --git a/src/feature/ffmpeg/ffmpeg-encoder.c b/src/feature/ffmpeg/ffmpeg-encoder.c index ec67bcfab..b327925a3 100644 --- a/src/feature/ffmpeg/ffmpeg-encoder.c +++ b/src/feature/ffmpeg/ffmpeg-encoder.c @@ -11,6 +11,9 @@ #include #include +#include +#include + #include #if LIBAVUTIL_VERSION_MAJOR >= 53 #include @@ -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); - _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 @@ 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); } diff --git a/src/feature/ffmpeg/ffmpeg-encoder.h b/src/feature/ffmpeg/ffmpeg-encoder.h index a5215b8a5..49d386cdc 100644 --- a/src/feature/ffmpeg/ffmpeg-encoder.h +++ b/src/feature/ffmpeg/ffmpeg-encoder.h @@ -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*); diff --git a/src/feature/imagemagick/imagemagick-gif-encoder.c b/src/feature/imagemagick/imagemagick-gif-encoder.c deleted file mode 100644 index 8d8ebcb5c..000000000 --- a/src/feature/imagemagick/imagemagick-gif-encoder.c +++ /dev/null @@ -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 -#include -#include - -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; -} diff --git a/src/feature/imagemagick/imagemagick-gif-encoder.h b/src/feature/imagemagick/imagemagick-gif-encoder.h deleted file mode 100644 index 67afdfe90..000000000 --- a/src/feature/imagemagick/imagemagick-gif-encoder.h +++ /dev/null @@ -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 - -CXX_GUARD_START - -#include - -#if MAGICKWAND_VERSION_MAJOR >= 7 -#include -#else -#include -#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 diff --git a/src/platform/qt/GIFView.cpp b/src/platform/qt/GIFView.cpp index 901e6c365..8a6d47fc7 100644 --- a/src/platform/qt/GIFView.cpp +++ b/src/platform/qt/GIFView.cpp @@ -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 -#include -#include - 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(&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 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 diff --git a/src/platform/qt/GIFView.h b/src/platform/qt/GIFView.h index b138cf071..fc4ebd1a3 100644 --- a/src/platform/qt/GIFView.h +++ b/src/platform/qt/GIFView.h @@ -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 @@ -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; }; diff --git a/src/platform/qt/GIFView.ui b/src/platform/qt/GIFView.ui index daa5de534..57dab1cd7 100644 --- a/src/platform/qt/GIFView.ui +++ b/src/platform/qt/GIFView.ui @@ -6,18 +6,52 @@ 0 0 - 278 - 247 + 392 + 220 Record GIF - + QLayout::SetFixedSize - + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Frameskip + + + + + + + 2 + + + + + + + QDialogButtonBox::Close + + + + @@ -51,6 +85,19 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -74,81 +121,8 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - Frameskip - - - - - - - 2 - - - - - - - Frame delay (ms) - - - - - - - Automatic - - - true - - - - - - - false - - - 5000 - - - 50 - - - - - - - - - - QDialogButtonBox::Close - - - @@ -169,21 +143,5 @@ - - delayAuto - clicked(bool) - delayMs - setDisabled(bool) - - - 202 - 177 - - - 192 - 148 - - - diff --git a/src/platform/qt/VideoView.cpp b/src/platform/qt/VideoView.cpp index 9ef896423..42d00f587 100644 --- a/src/platform/qt/VideoView.cpp +++ b/src/platform/qt/VideoView.cpp @@ -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(); diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index eedd43ac0..05026660b 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -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); } diff --git a/src/platform/qt/Window.h b/src/platform/qt/Window.h index 251550c6d..a7a70f48f 100644 --- a/src/platform/qt/Window.h +++ b/src/platform/qt/Window.h @@ -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