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");