From 3f75078174dd7cd7f696b59c3e43bd2842c6359c Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 23 Jul 2020 21:42:35 -0700 Subject: [PATCH 1/3] Util: Factor out gcd code --- include/mgba-util/math.h | 13 +++++++++++++ src/platform/qt/VideoView.cpp | 12 +++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/include/mgba-util/math.h b/include/mgba-util/math.h index eaf2087a2..9e239869c 100644 --- a/include/mgba-util/math.h +++ b/include/mgba-util/math.h @@ -58,6 +58,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/platform/qt/VideoView.cpp b/src/platform/qt/VideoView.cpp index 307245aa8..da09fc150 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; @@ -396,15 +398,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); } From cdcbedc65b942e006775fb105363a82b6eb2f91e Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 23 Jul 2020 21:55:05 -0700 Subject: [PATCH 2/3] FFmpeg: Backport and improve some API from medusa --- src/feature/ffmpeg/ffmpeg-encoder.c | 15 +++++++++++++-- src/feature/ffmpeg/ffmpeg-encoder.h | 3 +++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/feature/ffmpeg/ffmpeg-encoder.c b/src/feature/ffmpeg/ffmpeg-encoder.c index 04501c901..948822b26 100644 --- a/src/feature/ffmpeg/ffmpeg-encoder.c +++ b/src/feature/ffmpeg/ffmpeg-encoder.c @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -92,6 +93,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) { @@ -363,8 +365,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 * encoder->frameskip, GBA_ARM7TDMI_FREQUENCY }; - encoder->video->framerate = (AVRational) { GBA_ARM7TDMI_FREQUENCY, VIDEO_TOTAL_LENGTH * encoder->frameskip }; + encoder->video->time_base = (AVRational) { encoder->frameCycles * encoder->frameskip, encoder->cycles }; + encoder->video->framerate = (AVRational) { encoder->cycles, encoder->frameCycles * encoder->frameskip }; encoder->videoStream->time_base = encoder->video->time_base; encoder->videoStream->avg_frame_rate = encoder->video->framerate; encoder->video->pix_fmt = encoder->pixFormat; @@ -821,3 +823,12 @@ static void _ffmpegSetVideoDimensions(struct mAVStream* stream, unsigned width, encoder->videoFrame->width, encoder->videoFrame->height, encoder->videoFrame->format, SWS_POINT, 0, 0, 0); } + +void FFmpegEncoderSetInputFrameRate(struct FFmpegEncoder* encoder, int numerator, int denominator) { + reduceFraction(&numerator, &denominator); + encoder->frameCycles = numerator; + encoder->cycles = denominator; + if (encoder->video) { + encoder->video->framerate = (AVRational) { denominator, numerator * encoder->frameskip }; + } +} \ No newline at end of file diff --git a/src/feature/ffmpeg/ffmpeg-encoder.h b/src/feature/ffmpeg/ffmpeg-encoder.h index 9c2366277..746e8c46e 100644 --- a/src/feature/ffmpeg/ffmpeg-encoder.h +++ b/src/feature/ffmpeg/ffmpeg-encoder.h @@ -78,6 +78,8 @@ struct FFmpegEncoder { int height; int iwidth; int iheight; + int frameCycles; + int cycles; int frameskip; int skipResidue; bool loop; @@ -97,6 +99,7 @@ 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); From d585370116dd32eef5c58f1d3b654d081374c103 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 23 Jul 2020 22:33:46 -0700 Subject: [PATCH 3/3] FFmpeg: Add WebP recording --- CHANGES | 2 +- README.md | 4 +- src/feature/ffmpeg/ffmpeg-encoder.c | 21 ++++++-- src/platform/qt/GIFView.cpp | 17 ++++-- src/platform/qt/GIFView.ui | 81 ++++++++++++++++------------- src/platform/qt/Window.cpp | 2 +- 6 files changed, 81 insertions(+), 46 deletions(-) diff --git a/CHANGES b/CHANGES index e249da41e..f49f441bb 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,7 @@ 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 bec608b75..9a649ee0c 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Features - Screenshot support. - Cheat code support. - 9 savestate slots. Savestates are also viewable as screenshots. -- 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. @@ -222,7 +222,7 @@ mGBA has no hard dependencies, however, the following optional dependencies are - 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/src/feature/ffmpeg/ffmpeg-encoder.c b/src/feature/ffmpeg/ffmpeg-encoder.c index 948822b26..1e3da858b 100644 --- a/src/feature/ffmpeg/ffmpeg-encoder.c +++ b/src/feature/ffmpeg/ffmpeg-encoder.c @@ -180,10 +180,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) { @@ -411,6 +413,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(); @@ -487,6 +493,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; @@ -755,7 +763,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); 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/Window.cpp b/src/platform/qt/Window.cpp index c139ebf31..5d3f4d920 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -1475,7 +1475,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");