Video: GIF encoder using ImageMagick

This commit is contained in:
Jeffrey Pfau 2014-11-19 03:19:35 -08:00
parent 0308f136c7
commit 888b64f8b5
11 changed files with 357 additions and 21 deletions

View File

@ -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:")

View File

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

View File

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

View File

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

100
src/platform/qt/GIFVIew.ui Normal file
View File

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

View File

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

49
src/platform/qt/GIFView.h Normal file
View File

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

View File

@ -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());

View File

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

View File

@ -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()));

View File

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