FFmpeg: Add WebP recording

This commit is contained in:
Vicki Pfau 2020-07-23 22:33:46 -07:00
parent cdcbedc65b
commit d585370116
6 changed files with 81 additions and 46 deletions

View File

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

View File

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

View File

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

View File

@ -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<CoreController> 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";
}

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>392</width>
<height>262</height>
<height>225</height>
</rect>
</property>
<property name="windowTitle">
@ -17,29 +17,20 @@
<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 @@
</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>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
<string>APNG</string>
</property>
<attribute name="buttonGroup">
<string notr="true">format</string>
</attribute>
</widget>
</item>
<item row="1" column="0">
@ -142,13 +129,36 @@
</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>stop</tabstop>
<tabstop>selectFile</tabstop>
<tabstop>fmtGif</tabstop>
<tabstop>fmtWebP</tabstop>
<tabstop>fmtApng</tabstop>
<tabstop>loop</tabstop>
<tabstop>frameskip</tabstop>

View File

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