FFmpeg: Add WebP recording
@@ -27,7 +27,7 @@ - Frameskip, configurable up to 10.
- 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 @@ - Qt 5: for the GUI frontend. Qt Multimedia or SDL are required for audio.
- 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.
@@ -180,10 +180,12 @@ { AV_PIX_FMT_RGB0, 3 },
{ 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 @@ if (strcmp(vcodec->name, "libvpx-vp9") == 0 && encoder->videoBitrate == 0) {
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 @@ if (strcmp(encoder->containerFormat, "gif") == 0) {
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 @@
#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);
@@ -27,6 +27,7 @@ connect(m_ui.selectFile, &QAbstractButton::clicked, this, &GIFView::selectFile);
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 @@ FFmpegEncoderSetDimensions(&m_encoder, size.width(), size.height());
} 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 @@ FFmpegEncoderSetVideo(&m_encoder, "gif", 0, m_ui.frameskip->value());
} 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 @@ m_ui.stop->setEnabled(false);
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 @@ if (filename.endsWith(".gif")) {
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 @@ filename.truncate(index);
} if (m_ui.fmtGif->isChecked()) { filename += ".gif"; + } else if (m_ui.fmtWebP->isChecked()) { + filename += ".webp"; } else if (m_ui.fmtApng->isChecked()) { filename += ".png"; }
@@ -7,7 +7,7 @@ <rect>
<x>0</x> <y>0</y> <width>392</width> - <height>262</height> + <height>225</height> </rect> </property> <property name="windowTitle">@@ -17,29 +17,20 @@ <layout class="QGridLayout" name="gridLayout_3">
<property name="sizeConstraint"> <enum>QLayout::SetFixedSize</enum> </property> - <item row="2" column="0"> - <widget class="QRadioButton" name="fmtApng"> - <property name="text"> - <string>APNG</string> + <item row="4" column="0" colspan="3"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> </property> - <attribute name="buttonGroup"> - <string notr="true">format</string> - </attribute> </widget> </item> - <item row="2" column="2"> - <widget class="QSpinBox" name="frameskip"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> + <item row="1" column="1" colspan="2"> + <widget class="QCheckBox" name="loop"> + <property name="text"> + <string>Loop</string> </property> - <property name="maximum"> - <number>9</number> - </property> - <property name="value"> - <number>2</number> + <property name="checked"> + <bool>true</bool> </property> </widget> </item>@@ -115,18 +106,14 @@ </widget>
</item> </layout> </item> - <item row="2" column="1"> - <widget class="QLabel" name="label"> + <item row="3" column="0"> + <widget class="QRadioButton" name="fmtApng"> <property name="text"> - <string>Frameskip</string> + <string>APNG</string> </property> - </widget> - </item> - <item row="3" column="0" colspan="3"> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="standardButtons"> - <set>QDialogButtonBox::Close</set> - </property> + <attribute name="buttonGroup"> + <string notr="true">format</string> + </attribute> </widget> </item> <item row="1" column="0">@@ -142,13 +129,36 @@ <string notr="true">format</string>
</attribute> </widget> </item> - <item row="1" column="1" colspan="2"> - <widget class="QCheckBox" name="loop"> + <item row="2" column="0"> + <widget class="QRadioButton" name="fmtWebP"> <property name="text"> - <string>Loop</string> + <string>WebP</string> </property> - <property name="checked"> - <bool>true</bool> + <attribute name="buttonGroup"> + <string notr="true">format</string> + </attribute> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Frameskip</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QSpinBox" name="frameskip"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximum"> + <number>9</number> + </property> + <property name="value"> + <number>2</number> </property> </widget> </item>@@ -160,6 +170,7 @@ <tabstop>start</tabstop>
<tabstop>stop</tabstop> <tabstop>selectFile</tabstop> <tabstop>fmtGif</tabstop> + <tabstop>fmtWebP</tabstop> <tabstop>fmtApng</tabstop> <tabstop>loop</tabstop> <tabstop>frameskip</tabstop>
@@ -1475,7 +1475,7 @@ #endif
#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");