diff --git a/CHANGES b/CHANGES index c1233dcaa..642e2cf5f 100644 --- a/CHANGES +++ b/CHANGES @@ -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 @@ Other fixes: - 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 diff --git a/src/feature/ffmpeg/ffmpeg-encoder.c b/src/feature/ffmpeg/ffmpeg-encoder.c index 3a67ca883..ca6272f4f 100644 --- a/src/feature/ffmpeg/ffmpeg-encoder.c +++ b/src/feature/ffmpeg/ffmpeg-encoder.c @@ -61,6 +61,7 @@ void FFmpegEncoderInit(struct FFmpegEncoder* encoder) { 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 @@ -228,6 +229,10 @@ void FFmpegEncoderSetDimensions(struct FFmpegEncoder* encoder, int width, int he encoder->height = height > 0 ? height : GBA_VIDEO_VERTICAL_PIXELS; } +void FFmpegEncoderSetLooping(struct FFmpegEncoder* encoder, bool loop) { + encoder->loop = loop; +} + bool FFmpegEncoderVerifyContainer(struct FFmpegEncoder* encoder) { AVOutputFormat* oformat = av_guess_format(encoder->containerFormat, 0, 0); AVCodec* acodec = avcodec_find_encoder_by_name(encoder->audioCodec); @@ -469,6 +474,12 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) { #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; 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; diff --git a/src/feature/ffmpeg/ffmpeg-encoder.h b/src/feature/ffmpeg/ffmpeg-encoder.h index 49d386cdc..9c2366277 100644 --- a/src/feature/ffmpeg/ffmpeg-encoder.h +++ b/src/feature/ffmpeg/ffmpeg-encoder.h @@ -80,6 +80,7 @@ struct FFmpegEncoder { 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 a 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*); diff --git a/src/platform/qt/GIFView.cpp b/src/platform/qt/GIFView.cpp index eda022538..2ef882741 100644 --- a/src/platform/qt/GIFView.cpp +++ b/src/platform/qt/GIFView.cpp @@ -28,7 +28,6 @@ GIFView::GIFView(QWidget* parent) FFmpegEncoderInit(&m_encoder); FFmpegEncoderSetAudio(&m_encoder, nullptr, 0); - FFmpegEncoderSetContainer(&m_encoder, "gif"); } GIFView::~GIFView() { @@ -44,9 +43,16 @@ void GIFView::setController(std::shared_ptr controller) { } 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 @@ void GIFView::stopRecording() { } 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); } diff --git a/src/platform/qt/GIFView.ui b/src/platform/qt/GIFView.ui index 57dab1cd7..7c43319a2 100644 --- a/src/platform/qt/GIFView.ui +++ b/src/platform/qt/GIFView.ui @@ -7,50 +7,42 @@ 0 0 392 - 220 + 262 - Record GIF + Record GIF/APNG QLayout::SetFixedSize - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - + + - Frameskip + APNG + + format + - + + + + 0 + 0 + + + + 9 + 2 - - - - QDialogButtonBox::Close - - - @@ -123,8 +115,55 @@ + + + + Frameskip + + + + + + + QDialogButtonBox::Close + + + + + + + GIF + + + true + + + format + + + + + + + Loop + + + true + + + + + filename + start + stop + selectFile + fmtGif + fmtApng + loop + frameskip + @@ -144,4 +183,7 @@ + + + diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index 201c4cc28..ce74ccf5a 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -1455,7 +1455,7 @@ void Window::setupMenu(QMenuBar* menubar) { #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");