FFmpeg: Add APNG recording and looping support

This commit is contained in:
Vicki Pfau 2020-02-09 14:25:37 -08:00
parent a3857c7472
commit 2f643d7944
6 changed files with 95 additions and 31 deletions

View File

@ -1,4 +1,6 @@
0.9.0: (Future) 0.9.0: (Future)
Features:
- Add APNG recording
Emulation fixes: Emulation fixes:
- ARM: Fix ALU reading PC after shifting - ARM: Fix ALU reading PC after shifting
- ARM: Fix STR storing PC after address calculation - ARM: Fix STR storing PC after address calculation
@ -34,6 +36,7 @@ Other fixes:
- VFS: Fix handle leak when double-mapping (fixes mgba.io/i/1659) - VFS: Fix handle leak when double-mapping (fixes mgba.io/i/1659)
Misc: Misc:
- FFmpeg: Add more presets - FFmpeg: Add more presets
- FFmpeg: Add looping option for GIF/APNG
- Qt: Renderer can be changed while a game is running - Qt: Renderer can be changed while a game is running
- Qt: Fix non-SDL build (fixes mgba.io/i/1656) - Qt: Fix non-SDL build (fixes mgba.io/i/1656)
- Switch: Make OpenGL scale adjustable while running - Switch: Make OpenGL scale adjustable while running

View File

@ -61,6 +61,7 @@ void FFmpegEncoderInit(struct FFmpegEncoder* encoder) {
encoder->iheight = GBA_VIDEO_VERTICAL_PIXELS; encoder->iheight = GBA_VIDEO_VERTICAL_PIXELS;
encoder->frameskip = 1; encoder->frameskip = 1;
encoder->skipResidue = 0; encoder->skipResidue = 0;
encoder->loop = false;
encoder->ipixFormat = encoder->ipixFormat =
#ifdef COLOR_16_BIT #ifdef COLOR_16_BIT
#ifdef COLOR_5_6_5 #ifdef COLOR_5_6_5
@ -228,6 +229,10 @@ void FFmpegEncoderSetDimensions(struct FFmpegEncoder* encoder, int width, int he
encoder->height = height > 0 ? height : GBA_VIDEO_VERTICAL_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) { bool FFmpegEncoderVerifyContainer(struct FFmpegEncoder* encoder) {
AVOutputFormat* oformat = av_guess_format(encoder->containerFormat, 0, 0); AVOutputFormat* oformat = av_guess_format(encoder->containerFormat, 0, 0);
AVCodec* acodec = avcodec_find_encoder_by_name(encoder->audioCodec); AVCodec* acodec = avcodec_find_encoder_by_name(encoder->audioCodec);
@ -469,6 +474,12 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
#endif #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; AVDictionary* opts = 0;
av_dict_set(&opts, "strict", "-2", 0); av_dict_set(&opts, "strict", "-2", 0);
bool res = avio_open(&encoder->context->pb, outfile, AVIO_FLAG_WRITE) < 0 || avformat_write_header(encoder->context, &opts) < 0; bool res = avio_open(&encoder->context->pb, outfile, AVIO_FLAG_WRITE) < 0 || avformat_write_header(encoder->context, &opts) < 0;

View File

@ -80,6 +80,7 @@ struct FFmpegEncoder {
int iheight; int iheight;
int frameskip; int frameskip;
int skipResidue; int skipResidue;
bool loop;
int64_t currentVideoFrame; int64_t currentVideoFrame;
struct SwsContext* scaleContext; struct SwsContext* scaleContext;
struct AVStream* videoStream; struct AVStream* videoStream;
@ -96,6 +97,7 @@ bool FFmpegEncoderSetAudio(struct FFmpegEncoder*, const char* acodec, unsigned a
bool FFmpegEncoderSetVideo(struct FFmpegEncoder*, const char* vcodec, unsigned vbr, int frameskip); bool FFmpegEncoderSetVideo(struct FFmpegEncoder*, const char* vcodec, unsigned vbr, int frameskip);
bool FFmpegEncoderSetContainer(struct FFmpegEncoder*, const char* container); bool FFmpegEncoderSetContainer(struct FFmpegEncoder*, const char* container);
void FFmpegEncoderSetDimensions(struct FFmpegEncoder*, int width, int height); void FFmpegEncoderSetDimensions(struct FFmpegEncoder*, int width, int height);
void FFmpegEncoderSetLooping(struct FFmpegEncoder*, bool loop);
bool FFmpegEncoderVerifyContainer(struct FFmpegEncoder*); bool FFmpegEncoderVerifyContainer(struct FFmpegEncoder*);
bool FFmpegEncoderOpen(struct FFmpegEncoder*, const char* outfile); bool FFmpegEncoderOpen(struct FFmpegEncoder*, const char* outfile);
void FFmpegEncoderClose(struct FFmpegEncoder*); void FFmpegEncoderClose(struct FFmpegEncoder*);

View File

@ -28,7 +28,6 @@ GIFView::GIFView(QWidget* parent)
FFmpegEncoderInit(&m_encoder); FFmpegEncoderInit(&m_encoder);
FFmpegEncoderSetAudio(&m_encoder, nullptr, 0); FFmpegEncoderSetAudio(&m_encoder, nullptr, 0);
FFmpegEncoderSetContainer(&m_encoder, "gif");
} }
GIFView::~GIFView() { GIFView::~GIFView() {
@ -44,9 +43,16 @@ void GIFView::setController(std::shared_ptr<CoreController> controller) {
} }
void GIFView::startRecording() { 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())) { 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; return;
} }
m_ui.start->setEnabled(false); m_ui.start->setEnabled(false);
@ -64,7 +70,7 @@ void GIFView::stopRecording() {
} }
void GIFView::selectFile() { 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); m_ui.filename->setText(filename);
} }

View File

@ -7,50 +7,42 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>392</width> <width>392</width>
<height>220</height> <height>262</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Record GIF</string> <string>Record GIF/APNG</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout_3"> <layout class="QGridLayout" name="gridLayout_3">
<property name="sizeConstraint"> <property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum> <enum>QLayout::SetFixedSize</enum>
</property> </property>
<item row="1" column="0"> <item row="2" column="0">
<spacer name="horizontalSpacer_2"> <widget class="QRadioButton" name="fmtApng">
<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">
<property name="text"> <property name="text">
<string>Frameskip</string> <string>APNG</string>
</property> </property>
<attribute name="buttonGroup">
<string notr="true">format</string>
</attribute>
</widget> </widget>
</item> </item>
<item row="1" column="2"> <item row="2" column="2">
<widget class="QSpinBox" name="frameskip"> <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"> <property name="value">
<number>2</number> <number>2</number>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3"> <item row="0" column="0" colspan="3">
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="1" column="0"> <item row="1" column="0">
@ -123,8 +115,55 @@
</item> </item>
</layout> </layout>
</item> </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> </layout>
</widget> </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/> <resources/>
<connections> <connections>
<connection> <connection>
@ -144,4 +183,7 @@
</hints> </hints>
</connection> </connection>
</connections> </connections>
<buttongroups>
<buttongroup name="format"/>
</buttongroups>
</ui> </ui>

View File

@ -1455,7 +1455,7 @@ void Window::setupMenu(QMenuBar* menubar) {
#ifdef USE_FFMPEG #ifdef USE_FFMPEG
addGameAction(tr("Record A/V..."), "recordOutput", this, &Window::openVideoWindow, "av"); 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 #endif
m_actions.addSeparator("av"); m_actions.addSeparator("av");