diff --git a/CHANGES b/CHANGES
index aaba0edfe..1285e13b6 100644
--- a/CHANGES
+++ b/CHANGES
@@ -29,7 +29,7 @@ Misc:
0.9.0: (Future)
Features:
- e-Reader card scanning
- - Add APNG recording
+ - Add WebP and APNG recording
- Support for unlicensed Pokemon Jade/Diamond Game Boy mapper
Emulation fixes:
- ARM: Fix ALU reading PC after shifting
diff --git a/README.md b/README.md
index 9ea244b8d..2cf55067a 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ Features
- Screenshot support.
- Cheat code support[[2]](#dscaveat).
- 9 savestate slots. Savestates are also viewable as screenshots[[2]](#dscaveat).
-- Video, GIF and APNG recording.
+- Video, GIF, WebP, and APNG recording.
- e-Reader support.
- Remappable controls for both keyboards and gamepads.
- Loading from ZIP and 7z files.
@@ -240,7 +240,7 @@ medusa has no hard dependencies, however, the following optional dependencies ar
- SDL: for a more basic frontend and gamepad support in the Qt frontend. SDL 2 is recommended, but 1.2 is supported.
- zlib and libpng: for screenshot support and savestate-in-PNG support.
- libedit: for command-line debugger support.
-- ffmpeg or libav: for video and GIF recording.
+- ffmpeg or libav: for video, GIF, WebP, and APNG recording.
- libzip or zlib: for loading ROMs stored in zip files.
- SQLite3: for game databases.
- libelf: for ELF loading.
diff --git a/include/mgba-util/math.h b/include/mgba-util/math.h
index f308b4255..793312c9d 100644
--- a/include/mgba-util/math.h
+++ b/include/mgba-util/math.h
@@ -106,6 +106,19 @@ static inline uint32_t toPow2(uint32_t bits) {
return 1 << (32 - lz);
}
+static inline int reduceFraction(int* num, int* den) {
+ int n = *num;
+ int d = *den;
+ while (d != 0) {
+ int temp = n % d;
+ n = d;
+ d = temp;
+ }
+ *num /= n;
+ *den /= n;
+ return n;
+}
+
CXX_GUARD_END
#endif
diff --git a/src/feature/ffmpeg/ffmpeg-encoder.c b/src/feature/ffmpeg/ffmpeg-encoder.c
index 5e86b8712..a86dbc907 100644
--- a/src/feature/ffmpeg/ffmpeg-encoder.c
+++ b/src/feature/ffmpeg/ffmpeg-encoder.c
@@ -7,6 +7,7 @@
#include
#include
+#include
#include
#include
@@ -96,6 +97,7 @@ void FFmpegEncoderInit(struct FFmpegEncoder* encoder) {
encoder->source = NULL;
encoder->sink = NULL;
encoder->sinkFrame = NULL;
+ FFmpegEncoderSetInputFrameRate(encoder, VIDEO_TOTAL_LENGTH, GBA_ARM7TDMI_FREQUENCY);
int i;
for (i = 0; i < FFMPEG_FILTERS_MAX; ++i) {
@@ -182,10 +184,12 @@ bool FFmpegEncoderSetVideo(struct FFmpegEncoder* encoder, const char* vcodec, un
{ AV_PIX_FMT_0BGR, 3 },
{ AV_PIX_FMT_0RGB, 3 },
#endif
- { AV_PIX_FMT_YUV422P, 4 },
+ { AV_PIX_FMT_RGB32, 4},
+ { AV_PIX_FMT_BGR32, 4},
{ AV_PIX_FMT_YUV444P, 5 },
- { AV_PIX_FMT_YUV420P, 6 },
- { AV_PIX_FMT_PAL8, 7 },
+ { AV_PIX_FMT_YUV422P, 6 },
+ { AV_PIX_FMT_YUV420P, 7 },
+ { AV_PIX_FMT_PAL8, 8 },
};
if (!vcodec) {
@@ -413,6 +417,10 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
av_opt_set(encoder->video->priv_data, "lossless", "1", 0);
encoder->video->pix_fmt = AV_PIX_FMT_YUV444P;
}
+ if (strcmp(vcodec->name, "libwebp_anim") == 0 && encoder->videoBitrate == 0) {
+ av_opt_set(encoder->video->priv_data, "lossless", "1", 0);
+ encoder->video->pix_fmt = AV_PIX_FMT_RGB32;
+ }
if (encoder->pixFormat == AV_PIX_FMT_PAL8) {
encoder->graph = avfilter_graph_alloc();
@@ -489,6 +497,8 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
av_opt_set(encoder->context->priv_data, "loop", encoder->loop ? "0" : "-1", 0);
} else if (strcmp(encoder->containerFormat, "apng") == 0) {
av_opt_set(encoder->context->priv_data, "plays", encoder->loop ? "0" : "1", 0);
+ } else if (strcmp(encoder->containerFormat, "webp") == 0) {
+ av_opt_set(encoder->context->priv_data, "loop", encoder->loop ? "0" : "1", 0);
}
AVDictionary* opts = 0;
@@ -757,7 +767,12 @@ void _ffmpegPostVideoFrame(struct mAVStream* stream, const color_t* pixels, size
#if LIBAVCODEC_VERSION_MAJOR >= 55
av_frame_make_writable(encoder->videoFrame);
#endif
- encoder->videoFrame->pts = av_rescale_q(encoder->currentVideoFrame, encoder->video->time_base, encoder->videoStream->time_base);
+ if (encoder->video->codec->id == AV_CODEC_ID_WEBP) {
+ // TODO: Figure out why WebP is rescaling internally (should video frames not be rescaled externally?)
+ encoder->videoFrame->pts = encoder->currentVideoFrame;
+ } else {
+ encoder->videoFrame->pts = av_rescale_q(encoder->currentVideoFrame, encoder->video->time_base, encoder->videoStream->time_base);
+ }
++encoder->currentVideoFrame;
sws_scale(encoder->scaleContext, (const uint8_t* const*) &pixels, (const int*) &stride, 0, encoder->iheight, encoder->videoFrame->data, encoder->videoFrame->linesize);
@@ -831,10 +846,11 @@ static void _ffmpegSetVideoFrameRate(struct mAVStream* stream, unsigned numerato
FFmpegEncoderSetInputFrameRate(encoder, numerator, denominator);
}
-void FFmpegEncoderSetInputFrameRate(struct FFmpegEncoder* encoder, unsigned numerator, unsigned denominator) {
+void FFmpegEncoderSetInputFrameRate(struct FFmpegEncoder* encoder, int numerator, int denominator) {
+ reduceFraction(&numerator, &denominator);
encoder->frameCycles = numerator;
encoder->cycles = denominator;
if (encoder->video) {
- encoder->video->time_base = (AVRational) { numerator, denominator };
+ encoder->video->framerate = (AVRational) { denominator, numerator * encoder->frameskip };
}
}
diff --git a/src/feature/ffmpeg/ffmpeg-encoder.h b/src/feature/ffmpeg/ffmpeg-encoder.h
index 17d22a933..746e8c46e 100644
--- a/src/feature/ffmpeg/ffmpeg-encoder.h
+++ b/src/feature/ffmpeg/ffmpeg-encoder.h
@@ -78,8 +78,8 @@ struct FFmpegEncoder {
int height;
int iwidth;
int iheight;
- unsigned frameCycles;
- unsigned cycles;
+ int frameCycles;
+ int cycles;
int frameskip;
int skipResidue;
bool loop;
@@ -99,14 +99,13 @@ bool FFmpegEncoderSetAudio(struct FFmpegEncoder*, const char* acodec, unsigned a
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);
+void FFmpegEncoderSetInputFrameRate(struct FFmpegEncoder*, int numerator, int denominator);
void FFmpegEncoderSetLooping(struct FFmpegEncoder*, bool loop);
bool FFmpegEncoderVerifyContainer(struct FFmpegEncoder*);
bool FFmpegEncoderOpen(struct FFmpegEncoder*, const char* outfile);
void FFmpegEncoderClose(struct FFmpegEncoder*);
bool FFmpegEncoderIsOpen(struct FFmpegEncoder*);
-void FFmpegEncoderSetInputFrameRate(struct FFmpegEncoder*, unsigned numerator, unsigned denominator);
-
CXX_GUARD_END
#endif
diff --git a/src/platform/qt/GIFView.cpp b/src/platform/qt/GIFView.cpp
index a71a4b81b..820ef9547 100644
--- a/src/platform/qt/GIFView.cpp
+++ b/src/platform/qt/GIFView.cpp
@@ -27,6 +27,7 @@ GIFView::GIFView(QWidget* parent)
connect(m_ui.filename, &QLineEdit::textChanged, this, &GIFView::setFilename);
connect(m_ui.fmtGif, &QAbstractButton::clicked, this, &GIFView::changeExtension);
connect(m_ui.fmtApng, &QAbstractButton::clicked, this, &GIFView::changeExtension);
+ connect(m_ui.fmtWebP, &QAbstractButton::clicked, this, &GIFView::changeExtension);
FFmpegEncoderInit(&m_encoder);
FFmpegEncoderSetAudio(&m_encoder, nullptr, 0);
@@ -45,7 +46,10 @@ void GIFView::setController(std::shared_ptr controller) {
}
void GIFView::startRecording() {
- if (m_ui.fmtApng->isChecked()) {
+ if (m_ui.fmtWebP->isChecked()) {
+ FFmpegEncoderSetContainer(&m_encoder, "webp");
+ FFmpegEncoderSetVideo(&m_encoder, "libwebp_anim", 0, m_ui.frameskip->value());
+ } else if (m_ui.fmtApng->isChecked()) {
FFmpegEncoderSetContainer(&m_encoder, "apng");
FFmpegEncoderSetVideo(&m_encoder, "apng", 0, m_ui.frameskip->value());
} else {
@@ -54,15 +58,17 @@ void GIFView::startRecording() {
}
FFmpegEncoderSetLooping(&m_encoder, m_ui.loop->isChecked());
if (!FFmpegEncoderOpen(&m_encoder, m_filename.toUtf8().constData())) {
- LOG(QT, ERROR) << tr("Failed to open output GIF or APNG file: %1").arg(m_filename);
+ LOG(QT, ERROR) << tr("Failed to open output file: %1").arg(m_filename);
return;
}
m_ui.start->setEnabled(false);
m_ui.stop->setEnabled(true);
m_ui.frameskip->setEnabled(false);
m_ui.loop->setEnabled(false);
+ m_ui.fmtWebP->setEnabled(false);
m_ui.fmtApng->setEnabled(false);
m_ui.fmtGif->setEnabled(false);
+ m_ui.fmtWebP->setEnabled(false);
emit recordingStarted(&m_encoder.d);
}
@@ -73,12 +79,13 @@ void GIFView::stopRecording() {
m_ui.start->setEnabled(!m_filename.isEmpty());
m_ui.frameskip->setEnabled(true);
m_ui.loop->setEnabled(true);
+ m_ui.fmtWebP->setEnabled(true);
m_ui.fmtApng->setEnabled(true);
m_ui.fmtGif->setEnabled(true);
}
void GIFView::selectFile() {
- QString filename = GBAApp::app()->getSaveFileName(this, tr("Select output file"), tr("Graphics Interchange Format (*.gif);;Animated Portable Network Graphics (*.png *.apng)"));
+ QString filename = GBAApp::app()->getSaveFileName(this, tr("Select output file"), tr("Graphics Interchange Format (*.gif);;Animated Portable Network Graphics (*.png *.webp *.apng)"));
m_ui.filename->setText(filename);
}
@@ -90,6 +97,8 @@ void GIFView::setFilename(const QString& filename) {
m_ui.fmtGif->setChecked(Qt::Checked);
} else if (filename.endsWith(".png") || filename.endsWith(".apng")) {
m_ui.fmtApng->setChecked(Qt::Checked);
+ } else if (filename.endsWith(".webp")) {
+ m_ui.fmtWebP->setChecked(Qt::Checked);
}
}
}
@@ -105,6 +114,8 @@ void GIFView::changeExtension() {
}
if (m_ui.fmtGif->isChecked()) {
filename += ".gif";
+ } else if (m_ui.fmtWebP->isChecked()) {
+ filename += ".webp";
} else if (m_ui.fmtApng->isChecked()) {
filename += ".png";
}
diff --git a/src/platform/qt/GIFView.ui b/src/platform/qt/GIFView.ui
index 7c43319a2..72810f461 100644
--- a/src/platform/qt/GIFView.ui
+++ b/src/platform/qt/GIFView.ui
@@ -7,7 +7,7 @@
0
0
392
- 262
+ 225
@@ -17,29 +17,20 @@
QLayout::SetFixedSize
- -
-
-
- APNG
+
-
+
+
+ QDialogButtonBox::Close
-
- format
-
- -
-
-
-
- 0
- 0
-
+
-
+
+
+ Loop
-
- 9
-
-
- 2
+
+ true
@@ -115,18 +106,14 @@
- -
-
+
-
+
- Frameskip
-
-
-
- -
-
-
- QDialogButtonBox::Close
+ APNG
+
+ format
+
-
@@ -142,13 +129,36 @@
- -
-
+
-
+
- Loop
+ WebP
-
- true
+
+ format
+
+
+
+ -
+
+
+ Frameskip
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 9
+
+
+ 2
@@ -160,6 +170,7 @@
stop
selectFile
fmtGif
+ fmtWebP
fmtApng
loop
frameskip
diff --git a/src/platform/qt/VideoView.cpp b/src/platform/qt/VideoView.cpp
index 82d583eac..5a5b6a605 100644
--- a/src/platform/qt/VideoView.cpp
+++ b/src/platform/qt/VideoView.cpp
@@ -10,6 +10,8 @@
#include "GBAApp.h"
#include "LogController.h"
+#include
+
#include
using namespace QGBA;
@@ -400,15 +402,7 @@ void VideoView::updateAspectRatio(int width, int height, bool force) {
} else {
int w = m_width;
int h = m_height;
- // Get greatest common divisor
- while (w != 0) {
- int temp = h % w;
- h = w;
- w = temp;
- }
- int gcd = h;
- w = m_width / gcd;
- h = m_height / gcd;
+ reduceFraction(&h, &w);
safelySet(m_ui.wratio, w);
safelySet(m_ui.hratio, h);
}
diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp
index 2320d36ea..4611cc8be 100644
--- a/src/platform/qt/Window.cpp
+++ b/src/platform/qt/Window.cpp
@@ -1567,7 +1567,7 @@ void Window::setupMenu(QMenuBar* menubar) {
#ifdef USE_FFMPEG
addGameAction(tr("Record A/V..."), "recordOutput", this, &Window::openVideoWindow, "av");
- addGameAction(tr("Record GIF/APNG..."), "recordGIF", this, &Window::openGIFWindow, "av");
+ addGameAction(tr("Record GIF/WebP/APNG..."), "recordGIF", this, &Window::openGIFWindow, "av");
#endif
m_actions.addSeparator("av");