diff --git a/src/platform/qt/GameController.cpp b/src/platform/qt/GameController.cpp index 0b6de4090..08ae04c42 100644 --- a/src/platform/qt/GameController.cpp +++ b/src/platform/qt/GameController.cpp @@ -311,6 +311,26 @@ void GameController::setTurbo(bool set, bool forced) { } } +void GameController::setAVStream(GBAAVStream* stream) { + if (m_gameOpen) { + GBAThreadInterrupt(&m_threadContext); + m_threadContext.stream = stream; + GBAThreadContinue(&m_threadContext); + } else { + m_threadContext.stream = stream; + } +} + +void GameController::clearAVStream() { + if (m_gameOpen) { + GBAThreadInterrupt(&m_threadContext); + m_threadContext.stream = nullptr; + GBAThreadContinue(&m_threadContext); + } else { + m_threadContext.stream = nullptr; + } +} + void GameController::updateKeys() { int activeKeys = m_activeKeys; #ifdef BUILD_SDL diff --git a/src/platform/qt/GameController.h b/src/platform/qt/GameController.h index 45e2e07da..95cbc0da6 100644 --- a/src/platform/qt/GameController.h +++ b/src/platform/qt/GameController.h @@ -73,6 +73,8 @@ public slots: void setAudioSync(bool); void setFrameskip(int); void setTurbo(bool, bool forced = true); + void setAVStream(GBAAVStream*); + void clearAVStream(); #ifdef BUILD_SDL private slots: diff --git a/src/platform/qt/VideoView.cpp b/src/platform/qt/VideoView.cpp index 73b858533..4060a8457 100644 --- a/src/platform/qt/VideoView.cpp +++ b/src/platform/qt/VideoView.cpp @@ -2,14 +2,183 @@ #ifdef USE_FFMPEG +#include +#include + using namespace QGBA; +QMap VideoView::s_acodecMap; +QMap VideoView::s_vcodecMap; +QMap VideoView::s_containerMap; + VideoView::VideoView(QWidget* parent) : QWidget(parent) + , m_audioCodecCstr(nullptr) + , m_videoCodecCstr(nullptr) + , m_containerCstr(nullptr) { m_ui.setupUi(this); + if (s_acodecMap.empty()) { + s_acodecMap["mp3"] = "libmp3lame"; + s_acodecMap["uncompressed"] = "pcm_s16le"; + } + if (s_vcodecMap.empty()) { + s_vcodecMap["h264"] = "libx264rgb"; + } + if (s_containerMap.empty()) { + s_containerMap["mkv"] = "matroska"; + } + connect(m_ui.buttonBox, SIGNAL(rejected()), this, SLOT(close())); + connect(m_ui.start, SIGNAL(clicked()), this, SLOT(startRecording())); + connect(m_ui.stop, SIGNAL(clicked()), this, SLOT(stopRecording())); + + connect(m_ui.selectFile, SIGNAL(clicked()), this, SLOT(selectFile())); + connect(m_ui.filename, SIGNAL(textChanged(const QString&)), this, SLOT(setFilename(const QString&))); + + connect(m_ui.audio, SIGNAL(activated(const QString&)), this, SLOT(setAudioCodec(const QString&))); + connect(m_ui.video, SIGNAL(activated(const QString&)), this, SLOT(setVideoCodec(const QString&))); + connect(m_ui.container, SIGNAL(activated(const QString&)), this, SLOT(setContainer(const QString&))); + + connect(m_ui.abr, SIGNAL(valueChanged(int)), this, SLOT(setAudioBitrate(int))); + connect(m_ui.vbr, SIGNAL(valueChanged(int)), this, SLOT(setVideoBitrate(int))); + + FFmpegEncoderInit(&m_encoder); + + setAudioCodec(m_ui.audio->currentText()); + setVideoCodec(m_ui.video->currentText()); + setContainer(m_ui.container->currentText()); +} + +VideoView::~VideoView() { + stopRecording(); + free(m_audioCodecCstr); + free(m_videoCodecCstr); + free(m_containerCstr); +} + +void VideoView::startRecording() { + if (!validateSettings()) { + return; + } + if (!FFmpegEncoderOpen(&m_encoder, m_filename.toLocal8Bit().constData())) { + return; + } + m_ui.start->setEnabled(false); + m_ui.stop->setEnabled(true); + emit recordingStarted(&m_encoder.d); +} + +void VideoView::stopRecording() { + emit recordingStopped(); + FFmpegEncoderClose(&m_encoder); + m_ui.stop->setEnabled(false); + validateSettings(); +} + +void VideoView::selectFile() { + QString filename = QFileDialog::getSaveFileName(this, tr("Select output file")); + if (!filename.isEmpty()) { + m_ui.filename->setText(filename); + } +} + +void VideoView::setFilename(const QString& fname) { + m_filename = fname; + validateSettings(); +} + +void VideoView::setAudioCodec(const QString& codec) { + free(m_audioCodecCstr); + m_audioCodec = sanitizeCodec(codec); + if (s_acodecMap.contains(m_audioCodec)) { + m_audioCodec = s_acodecMap[m_audioCodec]; + } + m_audioCodecCstr = strdup(m_audioCodec.toLocal8Bit().constData()); + if (!FFmpegEncoderSetAudio(&m_encoder, m_audioCodecCstr, m_abr)) { + free(m_audioCodecCstr); + m_audioCodecCstr = nullptr; + } + validateSettings(); +} + +void VideoView::setVideoCodec(const QString& codec) { + free(m_videoCodecCstr); + m_videoCodec = sanitizeCodec(codec); + if (s_vcodecMap.contains(m_videoCodec)) { + m_videoCodec = s_vcodecMap[m_videoCodec]; + } + m_videoCodecCstr = strdup(m_videoCodec.toLocal8Bit().constData()); + if (!FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr)) { + free(m_videoCodecCstr); + m_videoCodecCstr = nullptr; + } + validateSettings(); +} + +void VideoView::setContainer(const QString& container) { + free(m_containerCstr); + m_container = sanitizeCodec(container); + if (s_containerMap.contains(m_container)) { + m_container = s_containerMap[m_container]; + } + m_containerCstr = strdup(m_container.toLocal8Bit().constData()); + if (!FFmpegEncoderSetContainer(&m_encoder, m_containerCstr)) { + free(m_containerCstr); + m_containerCstr = nullptr; + } + validateSettings(); +} + +void VideoView::setAudioBitrate(int br) { + m_abr = br; + FFmpegEncoderSetAudio(&m_encoder, m_audioCodecCstr, m_abr); + validateSettings(); +} + +void VideoView::setVideoBitrate(int br) { + m_abr = br; + FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr); + validateSettings(); +} + +bool VideoView::validateSettings() { + bool valid = true; + if (!m_audioCodecCstr) { + valid = false; + m_ui.audio->setStyleSheet("QComboBox { color: red; }"); + } else { + m_ui.audio->setStyleSheet(""); + } + + if (!m_videoCodecCstr) { + valid = false; + m_ui.video->setStyleSheet("QComboBox { color: red; }"); + } else { + m_ui.video->setStyleSheet(""); + } + + if (!m_containerCstr) { + valid = false; + m_ui.container->setStyleSheet("QComboBox { color: red; }"); + } else { + m_ui.container->setStyleSheet(""); + } + + // This |valid| check is necessary as if one of the cstrs + // is null, the encoder likely has a dangling pointer + if (valid && !FFmpegEncoderVerifyContainer(&m_encoder)) { + valid = false; + } + + m_ui.start->setEnabled(valid && !m_filename.isNull()); + return valid; +} + +QString VideoView::sanitizeCodec(const QString& codec) { + QString sanitized = codec.toLower(); + return sanitized.remove(QChar('.')); } #endif diff --git a/src/platform/qt/VideoView.h b/src/platform/qt/VideoView.h index c99325d41..2a53b9a8a 100644 --- a/src/platform/qt/VideoView.h +++ b/src/platform/qt/VideoView.h @@ -7,7 +7,9 @@ #include "ui_VideoView.h" -struct FFmpegEncoder; +extern "C" { +#include "platform/ffmpeg/ffmpeg-encoder.h" +} namespace QGBA { @@ -16,11 +18,50 @@ Q_OBJECT public: VideoView(QWidget* parent = nullptr); + virtual ~VideoView(); + + GBAAVStream* getStream() { return &m_encoder.d; } + +public slots: + void startRecording(); + void stopRecording(); + +signals: + void recordingStarted(GBAAVStream*); + void recordingStopped(); + +private slots: + void selectFile(); + void setFilename(const QString&); + void setAudioCodec(const QString&); + void setVideoCodec(const QString&); + void setContainer(const QString&); + + void setAudioBitrate(int); + void setVideoBitrate(int); private: + bool validateSettings(); + static QString sanitizeCodec(const QString&); + Ui::VideoView m_ui; - FFmpegEncoder* m_encoder; + FFmpegEncoder m_encoder; + + QString m_filename; + QString m_audioCodec; + QString m_videoCodec; + QString m_container; + char* m_audioCodecCstr; + char* m_videoCodecCstr; + char* m_containerCstr; + + int m_abr; + int m_vbr; + + static QMap s_acodecMap; + static QMap s_vcodecMap; + static QMap s_containerMap; }; } diff --git a/src/platform/qt/VideoView.ui b/src/platform/qt/VideoView.ui index 862f7b867..91ab16e1c 100644 --- a/src/platform/qt/VideoView.ui +++ b/src/platform/qt/VideoView.ui @@ -68,12 +68,7 @@ - Xvid - - - - - Uncompressed + FFV1 diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index d92a57256..fe8f4265d 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -148,6 +148,8 @@ void Window::selectPatch() { void Window::openVideoWindow() { if (!m_videoView) { m_videoView = new VideoView(); + connect(m_videoView, SIGNAL(recordingStarted(GBAAVStream*)), m_controller, SLOT(setAVStream(GBAAVStream*))); + connect(m_videoView, SIGNAL(recordingStopped()), m_controller, SLOT(clearAVStream()), Qt::DirectConnection); } m_videoView->show(); }