mirror of https://github.com/mgba-emu/mgba.git
Video: Allow GIF recording
This commit is contained in:
parent
5a6d09405d
commit
5b5c8c8d2d
|
@ -49,6 +49,12 @@ bool FFmpegEncoderSetAudio(struct FFmpegEncoder* encoder, const char* acodec, un
|
||||||
{ AV_SAMPLE_FMT_DBL, 4 },
|
{ AV_SAMPLE_FMT_DBL, 4 },
|
||||||
{ AV_SAMPLE_FMT_DBLP, 4 }
|
{ AV_SAMPLE_FMT_DBLP, 4 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!acodec) {
|
||||||
|
encoder->audioCodec = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
AVCodec* codec = avcodec_find_encoder_by_name(acodec);
|
AVCodec* codec = avcodec_find_encoder_by_name(acodec);
|
||||||
if (!codec) {
|
if (!codec) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -106,6 +112,8 @@ bool FFmpegEncoderSetVideo(struct FFmpegEncoder* encoder, const char* vcodec, un
|
||||||
{ AV_PIX_FMT_RGB0, 3 },
|
{ AV_PIX_FMT_RGB0, 3 },
|
||||||
{ AV_PIX_FMT_0BGR, 3 },
|
{ AV_PIX_FMT_0BGR, 3 },
|
||||||
{ AV_PIX_FMT_0RGB, 3 },
|
{ AV_PIX_FMT_0RGB, 3 },
|
||||||
|
{ AV_PIX_FMT_RGB8, 3 },
|
||||||
|
{ AV_PIX_FMT_BGR8, 3 },
|
||||||
{ AV_PIX_FMT_YUV422P, 4 },
|
{ AV_PIX_FMT_YUV422P, 4 },
|
||||||
{ AV_PIX_FMT_YUV444P, 5 },
|
{ AV_PIX_FMT_YUV444P, 5 },
|
||||||
{ AV_PIX_FMT_YUV420P, 6 }
|
{ AV_PIX_FMT_YUV420P, 6 }
|
||||||
|
@ -153,10 +161,10 @@ 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);
|
||||||
AVCodec* vcodec = avcodec_find_encoder_by_name(encoder->videoCodec);
|
AVCodec* vcodec = avcodec_find_encoder_by_name(encoder->videoCodec);
|
||||||
if (!acodec || !vcodec || !oformat) {
|
if ((encoder->audioCodec && !acodec) || !vcodec || !oformat) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!avformat_query_codec(oformat, acodec->id, FF_COMPLIANCE_EXPERIMENTAL)) {
|
if (encoder->audioCodec && !avformat_query_codec(oformat, acodec->id, FF_COMPLIANCE_EXPERIMENTAL)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!avformat_query_codec(oformat, vcodec->id, FF_COMPLIANCE_EXPERIMENTAL)) {
|
if (!avformat_query_codec(oformat, vcodec->id, FF_COMPLIANCE_EXPERIMENTAL)) {
|
||||||
|
@ -168,7 +176,7 @@ bool FFmpegEncoderVerifyContainer(struct FFmpegEncoder* encoder) {
|
||||||
bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
|
bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
|
||||||
AVCodec* acodec = avcodec_find_encoder_by_name(encoder->audioCodec);
|
AVCodec* acodec = avcodec_find_encoder_by_name(encoder->audioCodec);
|
||||||
AVCodec* vcodec = avcodec_find_encoder_by_name(encoder->videoCodec);
|
AVCodec* vcodec = avcodec_find_encoder_by_name(encoder->videoCodec);
|
||||||
if (!acodec || !vcodec || !FFmpegEncoderVerifyContainer(encoder)) {
|
if ((encoder->audioCodec && !acodec) || !vcodec || !FFmpegEncoderVerifyContainer(encoder)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,44 +189,46 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
|
||||||
|
|
||||||
encoder->context->oformat = av_guess_format(encoder->containerFormat, 0, 0);
|
encoder->context->oformat = av_guess_format(encoder->containerFormat, 0, 0);
|
||||||
|
|
||||||
encoder->audioStream = avformat_new_stream(encoder->context, acodec);
|
if (acodec) {
|
||||||
encoder->audio = encoder->audioStream->codec;
|
encoder->audioStream = avformat_new_stream(encoder->context, acodec);
|
||||||
encoder->audio->bit_rate = encoder->audioBitrate;
|
encoder->audio = encoder->audioStream->codec;
|
||||||
encoder->audio->channels = 2;
|
encoder->audio->bit_rate = encoder->audioBitrate;
|
||||||
encoder->audio->channel_layout = AV_CH_LAYOUT_STEREO;
|
encoder->audio->channels = 2;
|
||||||
encoder->audio->sample_rate = encoder->sampleRate;
|
encoder->audio->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||||
encoder->audio->sample_fmt = encoder->sampleFormat;
|
encoder->audio->sample_rate = encoder->sampleRate;
|
||||||
AVDictionary* opts = 0;
|
encoder->audio->sample_fmt = encoder->sampleFormat;
|
||||||
av_dict_set(&opts, "strict", "-2", 0);
|
AVDictionary* opts = 0;
|
||||||
if (encoder->context->oformat->flags & AVFMT_GLOBALHEADER) {
|
av_dict_set(&opts, "strict", "-2", 0);
|
||||||
encoder->audio->flags |= CODEC_FLAG_GLOBAL_HEADER;
|
if (encoder->context->oformat->flags & AVFMT_GLOBALHEADER) {
|
||||||
}
|
encoder->audio->flags |= CODEC_FLAG_GLOBAL_HEADER;
|
||||||
avcodec_open2(encoder->audio, acodec, &opts);
|
}
|
||||||
av_dict_free(&opts);
|
avcodec_open2(encoder->audio, acodec, &opts);
|
||||||
encoder->audioFrame = av_frame_alloc();
|
av_dict_free(&opts);
|
||||||
encoder->audioFrame->nb_samples = encoder->audio->frame_size;
|
encoder->audioFrame = av_frame_alloc();
|
||||||
encoder->audioFrame->format = encoder->audio->sample_fmt;
|
encoder->audioFrame->nb_samples = encoder->audio->frame_size;
|
||||||
encoder->audioFrame->pts = 0;
|
encoder->audioFrame->format = encoder->audio->sample_fmt;
|
||||||
encoder->resampleContext = avresample_alloc_context();
|
encoder->audioFrame->pts = 0;
|
||||||
av_opt_set_int(encoder->resampleContext, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0);
|
encoder->resampleContext = avresample_alloc_context();
|
||||||
av_opt_set_int(encoder->resampleContext, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0);
|
av_opt_set_int(encoder->resampleContext, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0);
|
||||||
av_opt_set_int(encoder->resampleContext, "in_sample_rate", PREFERRED_SAMPLE_RATE, 0);
|
av_opt_set_int(encoder->resampleContext, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0);
|
||||||
av_opt_set_int(encoder->resampleContext, "out_sample_rate", encoder->sampleRate, 0);
|
av_opt_set_int(encoder->resampleContext, "in_sample_rate", PREFERRED_SAMPLE_RATE, 0);
|
||||||
av_opt_set_int(encoder->resampleContext, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);
|
av_opt_set_int(encoder->resampleContext, "out_sample_rate", encoder->sampleRate, 0);
|
||||||
av_opt_set_int(encoder->resampleContext, "out_sample_fmt", encoder->sampleFormat, 0);
|
av_opt_set_int(encoder->resampleContext, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);
|
||||||
avresample_open(encoder->resampleContext);
|
av_opt_set_int(encoder->resampleContext, "out_sample_fmt", encoder->sampleFormat, 0);
|
||||||
encoder->audioBufferSize = (encoder->audioFrame->nb_samples * PREFERRED_SAMPLE_RATE / encoder->sampleRate) * 4;
|
avresample_open(encoder->resampleContext);
|
||||||
encoder->audioBuffer = av_malloc(encoder->audioBufferSize);
|
encoder->audioBufferSize = (encoder->audioFrame->nb_samples * PREFERRED_SAMPLE_RATE / encoder->sampleRate) * 4;
|
||||||
encoder->postaudioBufferSize = av_samples_get_buffer_size(0, encoder->audio->channels, encoder->audio->frame_size, encoder->audio->sample_fmt, 0);
|
encoder->audioBuffer = av_malloc(encoder->audioBufferSize);
|
||||||
encoder->postaudioBuffer = av_malloc(encoder->postaudioBufferSize);
|
encoder->postaudioBufferSize = av_samples_get_buffer_size(0, encoder->audio->channels, encoder->audio->frame_size, encoder->audio->sample_fmt, 0);
|
||||||
avcodec_fill_audio_frame(encoder->audioFrame, encoder->audio->channels, encoder->audio->sample_fmt, (const uint8_t*) encoder->postaudioBuffer, encoder->postaudioBufferSize, 0);
|
encoder->postaudioBuffer = av_malloc(encoder->postaudioBufferSize);
|
||||||
|
avcodec_fill_audio_frame(encoder->audioFrame, encoder->audio->channels, encoder->audio->sample_fmt, (const uint8_t*) encoder->postaudioBuffer, encoder->postaudioBufferSize, 0);
|
||||||
|
|
||||||
if (encoder->audio->codec->id == AV_CODEC_ID_AAC &&
|
if (encoder->audio->codec->id == AV_CODEC_ID_AAC &&
|
||||||
(strcasecmp(encoder->containerFormat, "mp4") ||
|
(strcasecmp(encoder->containerFormat, "mp4") ||
|
||||||
strcasecmp(encoder->containerFormat, "m4v") ||
|
strcasecmp(encoder->containerFormat, "m4v") ||
|
||||||
strcasecmp(encoder->containerFormat, "mov"))) {
|
strcasecmp(encoder->containerFormat, "mov"))) {
|
||||||
// MP4 container doesn't support the raw ADTS AAC format that the encoder spits out
|
// MP4 container doesn't support the raw ADTS AAC format that the encoder spits out
|
||||||
encoder->absf = av_bitstream_filter_init("aac_adtstoasc");
|
encoder->absf = av_bitstream_filter_init("aac_adtstoasc");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder->videoStream = avformat_new_stream(encoder->context, vcodec);
|
encoder->videoStream = avformat_new_stream(encoder->context, vcodec);
|
||||||
|
@ -268,25 +278,27 @@ void FFmpegEncoderClose(struct FFmpegEncoder* encoder) {
|
||||||
av_write_trailer(encoder->context);
|
av_write_trailer(encoder->context);
|
||||||
avio_close(encoder->context->pb);
|
avio_close(encoder->context->pb);
|
||||||
|
|
||||||
av_free(encoder->postaudioBuffer);
|
if (encoder->audioCodec) {
|
||||||
if (encoder->audioBuffer) {
|
av_free(encoder->postaudioBuffer);
|
||||||
av_free(encoder->audioBuffer);
|
if (encoder->audioBuffer) {
|
||||||
|
av_free(encoder->audioBuffer);
|
||||||
|
}
|
||||||
|
av_frame_free(&encoder->audioFrame);
|
||||||
|
avcodec_close(encoder->audio);
|
||||||
|
|
||||||
|
if (encoder->resampleContext) {
|
||||||
|
avresample_close(encoder->resampleContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoder->absf) {
|
||||||
|
av_bitstream_filter_close(encoder->absf);
|
||||||
|
encoder->absf = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
av_frame_free(&encoder->audioFrame);
|
|
||||||
avcodec_close(encoder->audio);
|
|
||||||
|
|
||||||
av_frame_free(&encoder->videoFrame);
|
av_frame_free(&encoder->videoFrame);
|
||||||
avcodec_close(encoder->video);
|
avcodec_close(encoder->video);
|
||||||
|
|
||||||
if (encoder->resampleContext) {
|
|
||||||
avresample_close(encoder->resampleContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encoder->absf) {
|
|
||||||
av_bitstream_filter_close(encoder->absf);
|
|
||||||
encoder->absf = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
sws_freeContext(encoder->scaleContext);
|
sws_freeContext(encoder->scaleContext);
|
||||||
|
|
||||||
avformat_free_context(encoder->context);
|
avformat_free_context(encoder->context);
|
||||||
|
@ -299,7 +311,7 @@ bool FFmpegEncoderIsOpen(struct FFmpegEncoder* encoder) {
|
||||||
|
|
||||||
void _ffmpegPostAudioFrame(struct GBAAVStream* stream, int32_t left, int32_t right) {
|
void _ffmpegPostAudioFrame(struct GBAAVStream* stream, int32_t left, int32_t right) {
|
||||||
struct FFmpegEncoder* encoder = (struct FFmpegEncoder*) stream;
|
struct FFmpegEncoder* encoder = (struct FFmpegEncoder*) stream;
|
||||||
if (!encoder->context) {
|
if (!encoder->context || !encoder->audioCodec) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -167,6 +167,16 @@ VideoView::VideoView(QWidget* parent)
|
||||||
.height = 160,
|
.height = 160,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addPreset(m_ui.presetGIF, (Preset) {
|
||||||
|
.container = "GIF",
|
||||||
|
.vcodec = "GIF",
|
||||||
|
.acodec = "None",
|
||||||
|
.vbr = 0,
|
||||||
|
.abr = 0,
|
||||||
|
.width = 240,
|
||||||
|
.height = 160,
|
||||||
|
});
|
||||||
|
|
||||||
setAudioCodec(m_ui.audio->currentText());
|
setAudioCodec(m_ui.audio->currentText());
|
||||||
setVideoCodec(m_ui.video->currentText());
|
setVideoCodec(m_ui.video->currentText());
|
||||||
setAudioBitrate(m_ui.abr->value());
|
setAudioBitrate(m_ui.abr->value());
|
||||||
|
@ -219,10 +229,15 @@ void VideoView::setFilename(const QString& fname) {
|
||||||
void VideoView::setAudioCodec(const QString& codec, bool manual) {
|
void VideoView::setAudioCodec(const QString& codec, bool manual) {
|
||||||
free(m_audioCodecCstr);
|
free(m_audioCodecCstr);
|
||||||
m_audioCodec = sanitizeCodec(codec, s_acodecMap);
|
m_audioCodec = sanitizeCodec(codec, s_acodecMap);
|
||||||
m_audioCodecCstr = strdup(m_audioCodec.toLocal8Bit().constData());
|
if (m_audioCodec == "none") {
|
||||||
|
m_audioCodecCstr = nullptr;
|
||||||
|
} else {
|
||||||
|
m_audioCodecCstr = strdup(m_audioCodec.toLocal8Bit().constData());
|
||||||
|
}
|
||||||
if (!FFmpegEncoderSetAudio(&m_encoder, m_audioCodecCstr, m_abr)) {
|
if (!FFmpegEncoderSetAudio(&m_encoder, m_audioCodecCstr, m_abr)) {
|
||||||
free(m_audioCodecCstr);
|
free(m_audioCodecCstr);
|
||||||
m_audioCodecCstr = nullptr;
|
m_audioCodecCstr = nullptr;
|
||||||
|
m_audioCodec = QString();
|
||||||
}
|
}
|
||||||
validateSettings();
|
validateSettings();
|
||||||
if (manual) {
|
if (manual) {
|
||||||
|
@ -237,6 +252,7 @@ void VideoView::setVideoCodec(const QString& codec, bool manual) {
|
||||||
if (!FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr)) {
|
if (!FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr)) {
|
||||||
free(m_videoCodecCstr);
|
free(m_videoCodecCstr);
|
||||||
m_videoCodecCstr = nullptr;
|
m_videoCodecCstr = nullptr;
|
||||||
|
m_videoCodec = QString();
|
||||||
}
|
}
|
||||||
validateSettings();
|
validateSettings();
|
||||||
if (manual) {
|
if (manual) {
|
||||||
|
@ -251,6 +267,7 @@ void VideoView::setContainer(const QString& container, bool manual) {
|
||||||
if (!FFmpegEncoderSetContainer(&m_encoder, m_containerCstr)) {
|
if (!FFmpegEncoderSetContainer(&m_encoder, m_containerCstr)) {
|
||||||
free(m_containerCstr);
|
free(m_containerCstr);
|
||||||
m_containerCstr = nullptr;
|
m_containerCstr = nullptr;
|
||||||
|
m_container = QString();
|
||||||
}
|
}
|
||||||
validateSettings();
|
validateSettings();
|
||||||
if (manual) {
|
if (manual) {
|
||||||
|
@ -316,21 +333,21 @@ void VideoView::showAdvanced(bool show) {
|
||||||
|
|
||||||
bool VideoView::validateSettings() {
|
bool VideoView::validateSettings() {
|
||||||
bool valid = !m_filename.isNull() && !FFmpegEncoderIsOpen(&m_encoder);
|
bool valid = !m_filename.isNull() && !FFmpegEncoderIsOpen(&m_encoder);
|
||||||
if (!m_audioCodecCstr) {
|
if (m_audioCodec.isNull()) {
|
||||||
valid = false;
|
valid = false;
|
||||||
m_ui.audio->setStyleSheet("QComboBox { color: red; }");
|
m_ui.audio->setStyleSheet("QComboBox { color: red; }");
|
||||||
} else {
|
} else {
|
||||||
m_ui.audio->setStyleSheet("");
|
m_ui.audio->setStyleSheet("");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_videoCodecCstr) {
|
if (m_videoCodec.isNull()) {
|
||||||
valid = false;
|
valid = false;
|
||||||
m_ui.video->setStyleSheet("QComboBox { color: red; }");
|
m_ui.video->setStyleSheet("QComboBox { color: red; }");
|
||||||
} else {
|
} else {
|
||||||
m_ui.video->setStyleSheet("");
|
m_ui.video->setStyleSheet("");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_containerCstr) {
|
if (m_container.isNull()) {
|
||||||
valid = false;
|
valid = false;
|
||||||
m_ui.container->setStyleSheet("QComboBox { color: red; }");
|
m_ui.container->setStyleSheet("QComboBox { color: red; }");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -146,6 +146,16 @@
|
||||||
</attribute>
|
</attribute>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="presetGIF">
|
||||||
|
<property name="text">
|
||||||
|
<string>GIF</string>
|
||||||
|
</property>
|
||||||
|
<attribute name="buttonGroup">
|
||||||
|
<string notr="true">presets</string>
|
||||||
|
</attribute>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
|
Loading…
Reference in New Issue