mirror of https://github.com/mgba-emu/mgba.git
FFmpeg: Add APNG recording and looping support
This commit is contained in:
parent
a3857c7472
commit
2f643d7944
3
CHANGES
3
CHANGES
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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*);
|
||||||
|
|
|
@ -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() {
|
||||||
|
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());
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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");
|
||||||
|
|
Loading…
Reference in New Issue