FFmpeg: Add APNG recording and looping support
@@ -1,4 +1,6 @@
0.9.0: (Future) +Features: + - Add APNG recording Emulation fixes: - ARM: Fix ALU reading PC after shifting - ARM: Fix STR storing PC after address calculation@@ -34,6 +36,7 @@ - Util: Fix crash reading invalid ELFs
- VFS: Fix handle leak when double-mapping (fixes mgba.io/i/1659) Misc: - FFmpeg: Add more presets + - FFmpeg: Add looping option for GIF/APNG - Qt: Renderer can be changed while a game is running - Qt: Fix non-SDL build (fixes mgba.io/i/1656) - Switch: Make OpenGL scale adjustable while running
@@ -61,6 +61,7 @@ encoder->iwidth = GBA_VIDEO_HORIZONTAL_PIXELS;
encoder->iheight = GBA_VIDEO_VERTICAL_PIXELS; encoder->frameskip = 1; encoder->skipResidue = 0; + encoder->loop = false; encoder->ipixFormat = #ifdef COLOR_16_BIT #ifdef COLOR_5_6_5@@ -226,6 +227,10 @@
void FFmpegEncoderSetDimensions(struct FFmpegEncoder* encoder, int width, int height) { encoder->width = width > 0 ? width : GBA_VIDEO_HORIZONTAL_PIXELS; encoder->height = height > 0 ? height : GBA_VIDEO_VERTICAL_PIXELS; +} + +void FFmpegEncoderSetLooping(struct FFmpegEncoder* encoder, bool loop) { + encoder->loop = loop; } bool FFmpegEncoderVerifyContainer(struct FFmpegEncoder* encoder) {@@ -467,6 +472,12 @@ av_image_alloc(encoder->videoFrame->data, encoder->videoFrame->linesize, encoder->videoFrame->width, encoder->videoFrame->height, encoder->videoFrame->format, 32);
#ifdef FFMPEG_USE_CODECPAR avcodec_parameters_from_context(encoder->videoStream->codecpar, encoder->video); #endif + } + + 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); } AVDictionary* opts = 0;
@@ -80,6 +80,7 @@ int iwidth;
int iheight; int frameskip; int skipResidue; + bool loop; int64_t currentVideoFrame; struct SwsContext* scaleContext; struct AVStream* videoStream;@@ -96,6 +97,7 @@ bool FFmpegEncoderSetAudio(struct FFmpegEncoder*, const char* acodec, unsigned abr);
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 FFmpegEncoderSetLooping(struct FFmpegEncoder*, bool loop); bool FFmpegEncoderVerifyContainer(struct FFmpegEncoder*); bool FFmpegEncoderOpen(struct FFmpegEncoder*, const char* outfile); void FFmpegEncoderClose(struct FFmpegEncoder*);
@@ -28,7 +28,6 @@ connect(m_ui.filename, &QLineEdit::textChanged, this, &GIFView::setFilename);
FFmpegEncoderInit(&m_encoder); FFmpegEncoderSetAudio(&m_encoder, nullptr, 0); - FFmpegEncoderSetContainer(&m_encoder, "gif"); } GIFView::~GIFView() {@@ -44,9 +43,16 @@ FFmpegEncoderSetDimensions(&m_encoder, size.width(), size.height());
} void GIFView::startRecording() { - FFmpegEncoderSetVideo(&m_encoder, "gif", 0, m_ui.frameskip->value()); + if (m_ui.fmtApng->isChecked()) { + FFmpegEncoderSetContainer(&m_encoder, "apng"); + FFmpegEncoderSetVideo(&m_encoder, "apng", 0, m_ui.frameskip->value()); + } else { + FFmpegEncoderSetContainer(&m_encoder, "gif"); + 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 file: %1").arg(m_filename); + LOG(QT, ERROR) << tr("Failed to open output GIF or APNG file: %1").arg(m_filename); return; } m_ui.start->setEnabled(false);@@ -64,7 +70,7 @@ m_ui.frameskip->setEnabled(true);
} void GIFView::selectFile() { - QString filename = GBAApp::app()->getSaveFileName(this, tr("Select output file"), tr("Graphics Interchange Format (*.gif)")); + QString filename = GBAApp::app()->getSaveFileName(this, tr("Select output file"), tr("Graphics Interchange Format (*.gif);;Animated Portable Network Graphics (*.png *.apng)")); m_ui.filename->setText(filename); }
@@ -7,47 +7,39 @@ <rect>
<x>0</x> <y>0</y> <width>392</width> - <height>220</height> + <height>262</height> </rect> </property> <property name="windowTitle"> - <string>Record GIF</string> + <string>Record GIF/APNG</string> </property> <layout class="QGridLayout" name="gridLayout_3"> <property name="sizeConstraint"> <enum>QLayout::SetFixedSize</enum> </property> - <item row="1" column="0"> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item row="1" column="1"> - <widget class="QLabel" name="label"> + <item row="2" column="0"> + <widget class="QRadioButton" name="fmtApng"> <property name="text"> - <string>Frameskip</string> + <string>APNG</string> </property> + <attribute name="buttonGroup"> + <string notr="true">format</string> + </attribute> </widget> </item> - <item row="1" column="2"> + <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> - <item row="2" column="0" colspan="3"> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="standardButtons"> - <set>QDialogButtonBox::Close</set> </property> </widget> </item>@@ -123,8 +115,55 @@ </widget>
</item> </layout> </item> + <item row="2" column="1"> + <widget class="QLabel" name="label"> + <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> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QRadioButton" name="fmtGif"> + <property name="text"> + <string>GIF</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + <attribute name="buttonGroup"> + <string notr="true">format</string> + </attribute> + </widget> + </item> + <item row="1" column="1" colspan="2"> + <widget class="QCheckBox" name="loop"> + <property name="text"> + <string>Loop</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> </layout> </widget> + <tabstops> + <tabstop>filename</tabstop> + <tabstop>start</tabstop> + <tabstop>stop</tabstop> + <tabstop>selectFile</tabstop> + <tabstop>fmtGif</tabstop> + <tabstop>fmtApng</tabstop> + <tabstop>loop</tabstop> + <tabstop>frameskip</tabstop> + </tabstops> <resources/> <connections> <connection>@@ -144,4 +183,7 @@ </hint>
</hints> </connection> </connections> + <buttongroups> + <buttongroup name="format"/> + </buttongroups> </ui>
@@ -1455,7 +1455,7 @@ #endif
#ifdef USE_FFMPEG addGameAction(tr("Record A/V..."), "recordOutput", this, &Window::openVideoWindow, "av"); - addGameAction(tr("Record GIF..."), "recordGIF", this, &Window::openGIFWindow, "av"); + addGameAction(tr("Record GIF/APNG..."), "recordGIF", this, &Window::openGIFWindow, "av"); #endif m_actions.addSeparator("av");