mirror of https://github.com/mgba-emu/mgba.git
Video: GIF encoder using ImageMagick
This commit is contained in:
parent
0308f136c7
commit
888b64f8b5
|
@ -7,6 +7,7 @@ set(USE_GDB_STUB ON CACHE BOOL "Whether or not to enable the GDB stub ARM debugg
|
|||
set(USE_FFMPEG ON CACHE BOOL "Whether or not to enable FFmpeg 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 ZIP support")
|
||||
set(USE_MAGICK ON CACHE BOOL "Whether or not to enable ImageMagick support")
|
||||
set(BUILD_QT ON CACHE BOOL "Build Qt frontend")
|
||||
set(BUILD_SDL ON CACHE BOOL "Build SDL frontend")
|
||||
set(BUILD_PERF OFF CACHE BOOL "Build performance profiling tool")
|
||||
|
@ -86,6 +87,7 @@ find_feature(USE_CLI_DEBUGGER "libedit")
|
|||
find_feature(USE_FFMPEG "libavcodec;libavformat;libavresample;libavutil;libswscale")
|
||||
find_feature(USE_PNG "ZLIB;PNG")
|
||||
find_feature(USE_LIBZIP "libzip")
|
||||
find_feature(USE_MAGICK "MagickWand")
|
||||
|
||||
include(CheckFunctionExists)
|
||||
check_function_exists(strndup HAVE_STRNDUP)
|
||||
|
@ -142,6 +144,14 @@ if(USE_FFMPEG)
|
|||
list(APPEND DEPENDENCY_LIB ${LIBAVCODEC_LIBRARIES} ${LIBAVFORMAT_LIBRARIES} ${LIBAVRESAMPLE_LIBRARIES} ${LIBAVUTIL_LIBRARIES} ${LIBSWSCALE_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(USE_MAGICK)
|
||||
add_definitions(-DUSE_MAGICK)
|
||||
include_directories(AFTER ${MAGICKWAND_INCLUDE_DIRS})
|
||||
link_directories(${MAGICKWAND_LIBRARY_DIRS})
|
||||
list(APPEND UTIL_SRC "${CMAKE_SOURCE_DIR}/src/platform/imagemagick/imagemagick-gif-encoder.c")
|
||||
list(APPEND DEPENDENCY_LIB ${MAGICKWAND_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(USE_PNG)
|
||||
add_definitions(-DUSE_PNG)
|
||||
include_directories(AFTER ${PNG_INCLUDE_DIRS})
|
||||
|
@ -195,6 +205,7 @@ message(STATUS "Feature summary:")
|
|||
message(STATUS " CLI debugger: ${USE_CLI_DEBUGGER}")
|
||||
message(STATUS " GDB stub: ${USE_GDB_STUB}")
|
||||
message(STATUS " Video recording: ${USE_FFMPEG}")
|
||||
message(STATUS " GIF recording: ${USE_MAGICK}")
|
||||
message(STATUS " Screenshot/advanced savestate support: ${USE_PNG}")
|
||||
message(STATUS " ZIP support: ${USE_LIBZIP}")
|
||||
message(STATUS "Frontend summary:")
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
#include "imagemagick-gif-encoder.h"
|
||||
|
||||
#include "gba-video.h"
|
||||
|
||||
static void _magickPostVideoFrame(struct GBAAVStream*, struct GBAVideoRenderer* renderer);
|
||||
static void _magickPostAudioFrame(struct GBAAVStream*, int32_t left, int32_t right);
|
||||
|
||||
void ImageMagickGIFEncoderInit(struct ImageMagickGIFEncoder* encoder) {
|
||||
encoder->wand = 0;
|
||||
|
||||
encoder->d.postVideoFrame = _magickPostVideoFrame;
|
||||
encoder->d.postAudioFrame = _magickPostAudioFrame;
|
||||
|
||||
encoder->frameskip = 2;
|
||||
}
|
||||
|
||||
bool ImageMagickGIFEncoderOpen(struct ImageMagickGIFEncoder* encoder, const char* outfile) {
|
||||
MagickWandGenesis();
|
||||
encoder->wand = NewMagickWand();
|
||||
encoder->outfile = strdup(outfile);
|
||||
encoder->frame = malloc(VIDEO_HORIZONTAL_PIXELS * VIDEO_VERTICAL_PIXELS * 4);
|
||||
encoder->currentFrame = 0;
|
||||
return true;
|
||||
}
|
||||
void ImageMagickGIFEncoderClose(struct ImageMagickGIFEncoder* encoder) {
|
||||
if (!encoder->wand) {
|
||||
return;
|
||||
}
|
||||
MagickWriteImages(encoder->wand, encoder->outfile, MagickTrue);
|
||||
free(encoder->outfile);
|
||||
free(encoder->frame);
|
||||
DestroyMagickWand(encoder->wand);
|
||||
encoder->wand = 0;
|
||||
MagickWandTerminus();
|
||||
}
|
||||
|
||||
bool ImageMagickGIFEncoderIsOpen(struct ImageMagickGIFEncoder* encoder) {
|
||||
return !!encoder->wand;
|
||||
}
|
||||
|
||||
static void _magickPostVideoFrame(struct GBAAVStream* stream, struct GBAVideoRenderer* renderer) {
|
||||
struct ImageMagickGIFEncoder* encoder = (struct ImageMagickGIFEncoder*) stream;
|
||||
|
||||
if (encoder->currentFrame % (encoder->frameskip + 1)) {
|
||||
++encoder->currentFrame;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t* pixels;
|
||||
unsigned stride;
|
||||
renderer->getPixels(renderer, &stride, (void**) &pixels);
|
||||
size_t row;
|
||||
for (row = 0; row < VIDEO_VERTICAL_PIXELS; ++row) {
|
||||
memcpy(&encoder->frame[row * VIDEO_HORIZONTAL_PIXELS], &pixels[row * 4 *stride], VIDEO_HORIZONTAL_PIXELS * 4);
|
||||
}
|
||||
|
||||
MagickConstituteImage(encoder->wand, VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS, "RGBP", CharPixel, encoder->frame);
|
||||
uint64_t ts = encoder->currentFrame;
|
||||
uint64_t nts = encoder->currentFrame + encoder->frameskip + 1;
|
||||
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 _magickPostAudioFrame(struct GBAAVStream* stream, int32_t left, int32_t right) {
|
||||
UNUSED(stream);
|
||||
UNUSED(left);
|
||||
UNUSED(right);
|
||||
// This is a video-only format...
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
#ifndef IMAGEMAGICK_GIF_ENCODER
|
||||
#define IMAGEMAGICK_GIF_ENCODER
|
||||
|
||||
#include "gba-thread.h"
|
||||
|
||||
#include <wand/MagickWand.h>
|
||||
|
||||
struct ImageMagickGIFEncoder {
|
||||
struct GBAAVStream d;
|
||||
MagickWand* wand;
|
||||
char* outfile;
|
||||
uint32_t* frame;
|
||||
|
||||
unsigned currentFrame;
|
||||
int frameskip;
|
||||
};
|
||||
|
||||
void ImageMagickGIFEncoderInit(struct ImageMagickGIFEncoder*);
|
||||
bool ImageMagickGIFEncoderOpen(struct ImageMagickGIFEncoder*, const char* outfile);
|
||||
void ImageMagickGIFEncoderClose(struct ImageMagickGIFEncoder*);
|
||||
bool ImageMagickGIFEncoderIsOpen(struct ImageMagickGIFEncoder*);
|
||||
|
||||
#endif
|
|
@ -35,6 +35,7 @@ set(SOURCE_FILES
|
|||
Display.cpp
|
||||
GBAApp.cpp
|
||||
GBAKeyEditor.cpp
|
||||
GIFView.cpp
|
||||
GameController.cpp
|
||||
InputController.cpp
|
||||
KeyEditor.cpp
|
||||
|
@ -46,6 +47,7 @@ set(SOURCE_FILES
|
|||
VideoView.cpp)
|
||||
|
||||
qt5_wrap_ui(UI_FILES
|
||||
GIFView.ui
|
||||
LoadSaveState.ui
|
||||
LogView.ui
|
||||
VideoView.ui)
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>GIFView</class>
|
||||
<widget class="QWidget" name="GIFView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>342</width>
|
||||
<height>124</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Record GIF</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="start">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Start</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="stop">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Stop</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="QPushButton" name="selectFile">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Select File</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="4">
|
||||
<widget class="QLineEdit" name="filename">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</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="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Close</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -0,0 +1,59 @@
|
|||
#include "GIFView.h"
|
||||
|
||||
#ifdef USE_MAGICK
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QMap>
|
||||
|
||||
using namespace QGBA;
|
||||
|
||||
GIFView::GIFView(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
connect(m_ui.buttonBox, SIGNAL(rejected()), this, SLOT(close()));
|
||||
connect(m_ui.start, SIGNAL(clicked()), this, SLOT(startRecording()));
|
||||
connect(m_ui.stop, SIGNAL(clicked()), this, SLOT(stopRecording()));
|
||||
|
||||
connect(m_ui.selectFile, SIGNAL(clicked()), this, SLOT(selectFile()));
|
||||
connect(m_ui.filename, SIGNAL(textChanged(const QString&)), this, SLOT(setFilename(const QString&)));
|
||||
|
||||
ImageMagickGIFEncoderInit(&m_encoder);
|
||||
}
|
||||
|
||||
GIFView::~GIFView() {
|
||||
stopRecording();
|
||||
}
|
||||
|
||||
void GIFView::startRecording() {
|
||||
if (!ImageMagickGIFEncoderOpen(&m_encoder, m_filename.toLocal8Bit().constData())) {
|
||||
return;
|
||||
}
|
||||
m_ui.start->setEnabled(false);
|
||||
m_ui.stop->setEnabled(true);
|
||||
emit recordingStarted(&m_encoder.d);
|
||||
}
|
||||
|
||||
void GIFView::stopRecording() {
|
||||
emit recordingStopped();
|
||||
ImageMagickGIFEncoderClose(&m_encoder);
|
||||
m_ui.stop->setEnabled(false);
|
||||
m_ui.start->setEnabled(true);
|
||||
}
|
||||
|
||||
void GIFView::selectFile() {
|
||||
QString filename = QFileDialog::getSaveFileName(this, tr("Select output file"));
|
||||
if (!filename.isEmpty()) {
|
||||
m_ui.filename->setText(filename);
|
||||
if (!ImageMagickGIFEncoderIsOpen(&m_encoder)) {
|
||||
m_ui.start->setEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GIFView::setFilename(const QString& fname) {
|
||||
m_filename = fname;
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,49 @@
|
|||
#ifndef QGBA_GIF_VIEW
|
||||
#define QGBA_GIF_VIEW
|
||||
|
||||
#ifdef USE_MAGICK
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "ui_GIFView.h"
|
||||
|
||||
extern "C" {
|
||||
#include "platform/imagemagick/imagemagick-gif-encoder.h"
|
||||
}
|
||||
|
||||
namespace QGBA {
|
||||
|
||||
class GIFView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
GIFView(QWidget* parent = nullptr);
|
||||
virtual ~GIFView();
|
||||
|
||||
GBAAVStream* getStream() { return &m_encoder.d; }
|
||||
|
||||
public slots:
|
||||
void startRecording();
|
||||
void stopRecording();
|
||||
|
||||
signals:
|
||||
void recordingStarted(GBAAVStream*);
|
||||
void recordingStopped();
|
||||
|
||||
private slots:
|
||||
void selectFile();
|
||||
void setFilename(const QString&);
|
||||
|
||||
private:
|
||||
Ui::GIFView m_ui;
|
||||
|
||||
ImageMagickGIFEncoder m_encoder;
|
||||
|
||||
QString m_filename;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
|
@ -167,16 +167,6 @@ VideoView::VideoView(QWidget* parent)
|
|||
.height = 160,
|
||||
});
|
||||
|
||||
addPreset(m_ui.presetGIF, (Preset) {
|
||||
.container = "GIF",
|
||||
.vcodec = "GIF",
|
||||
.acodec = "None",
|
||||
.vbr = 0,
|
||||
.abr = 0,
|
||||
.width = 240,
|
||||
.height = 160,
|
||||
});
|
||||
|
||||
setAudioCodec(m_ui.audio->currentText());
|
||||
setVideoCodec(m_ui.video->currentText());
|
||||
setAudioBitrate(m_ui.abr->value());
|
||||
|
|
|
@ -146,16 +146,6 @@
|
|||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="presetGIF">
|
||||
<property name="text">
|
||||
<string>GIF</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">presets</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include "GBAKeyEditor.h"
|
||||
#include "GDBController.h"
|
||||
#include "GDBWindow.h"
|
||||
#include "GIFView.h"
|
||||
#include "LoadSaveState.h"
|
||||
#include "LogView.h"
|
||||
#include "VideoView.h"
|
||||
|
@ -31,6 +32,9 @@ Window::Window(ConfigController* config, QWidget* parent)
|
|||
#ifdef USE_FFMPEG
|
||||
, m_videoView(nullptr)
|
||||
#endif
|
||||
#ifdef USE_MAGICK
|
||||
, m_gifView(nullptr)
|
||||
#endif
|
||||
#ifdef USE_GDB_STUB
|
||||
, m_gdbController(nullptr)
|
||||
#endif
|
||||
|
@ -71,6 +75,10 @@ Window::~Window() {
|
|||
#ifdef USE_FFMPEG
|
||||
delete m_videoView;
|
||||
#endif
|
||||
|
||||
#ifdef USE_MAGICK
|
||||
delete m_gifView;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Window::argumentsPassed(GBAArguments* args) {
|
||||
|
@ -163,6 +171,20 @@ void Window::openVideoWindow() {
|
|||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_MAGICK
|
||||
void Window::openGIFWindow() {
|
||||
if (!m_gifView) {
|
||||
m_gifView = new GIFView();
|
||||
connect(m_gifView, SIGNAL(recordingStarted(GBAAVStream*)), m_controller, SLOT(setAVStream(GBAAVStream*)));
|
||||
connect(m_gifView, SIGNAL(recordingStopped()), m_controller, SLOT(clearAVStream()), Qt::DirectConnection);
|
||||
connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_gifView, SLOT(stopRecording()));
|
||||
connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_gifView, SLOT(close()));
|
||||
connect(this, SIGNAL(shutdown()), m_gifView, SLOT(close()));
|
||||
}
|
||||
m_gifView->show();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_GDB_STUB
|
||||
void Window::gdbOpen() {
|
||||
if (!m_gdbController) {
|
||||
|
@ -456,7 +478,7 @@ void Window::setupMenu(QMenuBar* menubar) {
|
|||
fpsTargetOption->addValue(tr("240"), 240, target);
|
||||
m_config->updateOption("fpsTarget");
|
||||
|
||||
#if defined(USE_PNG) || defined(USE_FFMPEG)
|
||||
#if defined(USE_PNG) || defined(USE_FFMPEG) || defined(USE_MAGICK)
|
||||
avMenu->addSeparator();
|
||||
#endif
|
||||
|
||||
|
@ -477,6 +499,14 @@ void Window::setupMenu(QMenuBar* menubar) {
|
|||
avMenu->addAction(recordOutput);
|
||||
#endif
|
||||
|
||||
#ifdef USE_MAGICK
|
||||
QAction* recordGIF = new QAction(tr("Record GIF..."), avMenu);
|
||||
recordGIF->setShortcut(tr("Shift+F11"));
|
||||
connect(recordGIF, SIGNAL(triggered()), this, SLOT(openGIFWindow()));
|
||||
addAction(recordGIF);
|
||||
avMenu->addAction(recordGIF);
|
||||
#endif
|
||||
|
||||
QMenu* debuggingMenu = menubar->addMenu(tr("&Debugging"));
|
||||
QAction* viewLogs = new QAction(tr("View &logs..."), debuggingMenu);
|
||||
connect(viewLogs, SIGNAL(triggered()), m_logView, SLOT(show()));
|
||||
|
|
|
@ -20,6 +20,7 @@ namespace QGBA {
|
|||
|
||||
class ConfigController;
|
||||
class GameController;
|
||||
class GIFView;
|
||||
class LogView;
|
||||
class VideoView;
|
||||
class WindowBackground;
|
||||
|
@ -56,6 +57,10 @@ public slots:
|
|||
void openVideoWindow();
|
||||
#endif
|
||||
|
||||
#ifdef USE_MAGICK
|
||||
void openGIFWindow();
|
||||
#endif
|
||||
|
||||
#ifdef USE_GDB_STUB
|
||||
void gdbOpen();
|
||||
#endif
|
||||
|
@ -92,6 +97,10 @@ private:
|
|||
VideoView* m_videoView;
|
||||
#endif
|
||||
|
||||
#ifdef USE_MAGICK
|
||||
GIFView* m_gifView;
|
||||
#endif
|
||||
|
||||
#ifdef USE_GDB_STUB
|
||||
GDBController* m_gdbController;
|
||||
#endif
|
||||
|
|
Loading…
Reference in New Issue