Merge pull request #5083 from zhaowenlan1779/video-dumping-update

video_core, citra_qt: Video dumping updates
This commit is contained in:
Marshall Mohror
2020-04-03 21:15:32 -05:00
committed by GitHub
35 changed files with 2085 additions and 309 deletions

View File

@@ -162,6 +162,20 @@ add_executable(citra-qt
util/util.h
)
if (ENABLE_FFMPEG_VIDEO_DUMPER)
target_sources(citra-qt PRIVATE
dumping/dumping_dialog.cpp
dumping/dumping_dialog.h
dumping/dumping_dialog.ui
dumping/option_set_dialog.cpp
dumping/option_set_dialog.h
dumping/option_set_dialog.ui
dumping/options_dialog.cpp
dumping/options_dialog.h
dumping/options_dialog.ui
)
endif()
file(GLOB COMPAT_LIST
${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc
${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json)

View File

@@ -93,6 +93,7 @@ void Config::ReadValues() {
ReadMiscellaneousValues();
ReadDebuggingValues();
ReadWebServiceValues();
ReadVideoDumpingValues();
ReadUIValues();
ReadUtilityValues();
}
@@ -492,6 +493,49 @@ void Config::ReadSystemValues() {
qt_config->endGroup();
}
// Options for variable bit rate live streaming taken from here:
// https://developers.google.com/media/vp9/live-encoding
const QString DEFAULT_VIDEO_ENCODER_OPTIONS =
QStringLiteral("quality:realtime,speed:6,tile-columns:4,frame-parallel:1,threads:8,row-mt:1");
const QString DEFAULT_AUDIO_ENCODER_OPTIONS = QString{};
void Config::ReadVideoDumpingValues() {
qt_config->beginGroup(QStringLiteral("VideoDumping"));
Settings::values.output_format =
ReadSetting(QStringLiteral("output_format"), QStringLiteral("webm"))
.toString()
.toStdString();
Settings::values.format_options =
ReadSetting(QStringLiteral("format_options")).toString().toStdString();
Settings::values.video_encoder =
ReadSetting(QStringLiteral("video_encoder"), QStringLiteral("libvpx-vp9"))
.toString()
.toStdString();
Settings::values.video_encoder_options =
ReadSetting(QStringLiteral("video_encoder_options"), DEFAULT_VIDEO_ENCODER_OPTIONS)
.toString()
.toStdString();
Settings::values.video_bitrate =
ReadSetting(QStringLiteral("video_bitrate"), 2500000).toULongLong();
Settings::values.audio_encoder =
ReadSetting(QStringLiteral("audio_encoder"), QStringLiteral("libvorbis"))
.toString()
.toStdString();
Settings::values.audio_encoder_options =
ReadSetting(QStringLiteral("audio_encoder_options"), DEFAULT_AUDIO_ENCODER_OPTIONS)
.toString()
.toStdString();
Settings::values.audio_bitrate =
ReadSetting(QStringLiteral("audio_bitrate"), 64000).toULongLong();
qt_config->endGroup();
}
void Config::ReadUIValues() {
qt_config->beginGroup(QStringLiteral("UI"));
@@ -624,6 +668,7 @@ void Config::SaveValues() {
SaveMiscellaneousValues();
SaveDebuggingValues();
SaveWebServiceValues();
SaveVideoDumpingValues();
SaveUIValues();
SaveUtilityValues();
}
@@ -928,6 +973,33 @@ void Config::SaveSystemValues() {
qt_config->endGroup();
}
void Config::SaveVideoDumpingValues() {
qt_config->beginGroup(QStringLiteral("VideoDumping"));
WriteSetting(QStringLiteral("output_format"),
QString::fromStdString(Settings::values.output_format), QStringLiteral("webm"));
WriteSetting(QStringLiteral("format_options"),
QString::fromStdString(Settings::values.format_options));
WriteSetting(QStringLiteral("video_encoder"),
QString::fromStdString(Settings::values.video_encoder),
QStringLiteral("libvpx-vp9"));
WriteSetting(QStringLiteral("video_encoder_options"),
QString::fromStdString(Settings::values.video_encoder_options),
DEFAULT_VIDEO_ENCODER_OPTIONS);
WriteSetting(QStringLiteral("video_bitrate"),
static_cast<unsigned long long>(Settings::values.video_bitrate), 2500000);
WriteSetting(QStringLiteral("audio_encoder"),
QString::fromStdString(Settings::values.audio_encoder),
QStringLiteral("libvorbis"));
WriteSetting(QStringLiteral("audio_encoder_options"),
QString::fromStdString(Settings::values.audio_encoder_options),
DEFAULT_AUDIO_ENCODER_OPTIONS);
WriteSetting(QStringLiteral("audio_bitrate"),
static_cast<unsigned long long>(Settings::values.audio_bitrate), 64000);
qt_config->endGroup();
}
void Config::SaveUIValues() {
qt_config->beginGroup(QStringLiteral("UI"));

View File

@@ -44,6 +44,7 @@ private:
void ReadUpdaterValues();
void ReadUtilityValues();
void ReadWebServiceValues();
void ReadVideoDumpingValues();
void SaveValues();
void SaveAudioValues();
@@ -65,6 +66,7 @@ private:
void SaveUpdaterValues();
void SaveUtilityValues();
void SaveWebServiceValues();
void SaveVideoDumpingValues();
QVariant ReadSetting(const QString& name) const;
QVariant ReadSetting(const QString& name, const QVariant& default_value) const;

View File

@@ -0,0 +1,220 @@
// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <QFileDialog>
#include <QMessageBox>
#include "citra_qt/dumping/dumping_dialog.h"
#include "citra_qt/dumping/options_dialog.h"
#include "citra_qt/uisettings.h"
#include "core/settings.h"
#include "ui_dumping_dialog.h"
DumpingDialog::DumpingDialog(QWidget* parent)
: QDialog(parent), ui(std::make_unique<Ui::DumpingDialog>()) {
ui->setupUi(this);
format_generic_options = VideoDumper::GetFormatGenericOptions();
encoder_generic_options = VideoDumper::GetEncoderGenericOptions();
connect(ui->pathExplore, &QToolButton::clicked, this, &DumpingDialog::OnToolButtonClicked);
connect(ui->buttonBox, &QDialogButtonBox::accepted, [this] {
if (ui->pathLineEdit->text().isEmpty()) {
QMessageBox::critical(this, tr("Citra"), tr("Please specify the output path."));
return;
}
ApplyConfiguration();
accept();
});
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &DumpingDialog::reject);
connect(ui->formatOptionsButton, &QToolButton::clicked, [this] {
OpenOptionsDialog(formats.at(ui->formatComboBox->currentData().toUInt()).options,
format_generic_options, ui->formatOptionsLineEdit);
});
connect(ui->videoEncoderOptionsButton, &QToolButton::clicked, [this] {
OpenOptionsDialog(
video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).options,
encoder_generic_options, ui->videoEncoderOptionsLineEdit);
});
connect(ui->audioEncoderOptionsButton, &QToolButton::clicked, [this] {
OpenOptionsDialog(
audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).options,
encoder_generic_options, ui->audioEncoderOptionsLineEdit);
});
SetConfiguration();
connect(ui->formatComboBox, qOverload<int>(&QComboBox::currentIndexChanged), [this] {
ui->pathLineEdit->setText(QString{});
ui->formatOptionsLineEdit->clear();
PopulateEncoders();
});
connect(ui->videoEncoderComboBox, qOverload<int>(&QComboBox::currentIndexChanged),
[this] { ui->videoEncoderOptionsLineEdit->clear(); });
connect(ui->audioEncoderComboBox, qOverload<int>(&QComboBox::currentIndexChanged),
[this] { ui->audioEncoderOptionsLineEdit->clear(); });
}
DumpingDialog::~DumpingDialog() = default;
QString DumpingDialog::GetFilePath() const {
return ui->pathLineEdit->text();
}
void DumpingDialog::Populate() {
formats = VideoDumper::ListFormats();
video_encoders = VideoDumper::ListEncoders(AVMEDIA_TYPE_VIDEO);
audio_encoders = VideoDumper::ListEncoders(AVMEDIA_TYPE_AUDIO);
// Check that these are not empty
QString missing;
if (formats.empty()) {
missing = tr("output formats");
}
if (video_encoders.empty()) {
missing = tr("video encoders");
}
if (audio_encoders.empty()) {
missing = tr("audio encoders");
}
if (!missing.isEmpty()) {
QMessageBox::critical(this, tr("Citra"),
tr("Could not find any available %1.\nPlease check your FFmpeg "
"installation used for compilation.")
.arg(missing));
reject();
return;
}
// Populate formats
for (std::size_t i = 0; i < formats.size(); ++i) {
const auto& format = formats[i];
// Check format: only formats that have video encoders and audio encoders are displayed
bool has_video = false;
for (const auto& video_encoder : video_encoders) {
if (format.supported_video_codecs.count(video_encoder.codec)) {
has_video = true;
break;
}
}
if (!has_video)
continue;
bool has_audio = false;
for (const auto& audio_encoder : audio_encoders) {
if (format.supported_audio_codecs.count(audio_encoder.codec)) {
has_audio = true;
break;
}
}
if (!has_audio)
continue;
ui->formatComboBox->addItem(tr("%1 (%2)").arg(QString::fromStdString(format.long_name),
QString::fromStdString(format.name)),
static_cast<unsigned long long>(i));
if (format.name == Settings::values.output_format) {
ui->formatComboBox->setCurrentIndex(ui->formatComboBox->count() - 1);
}
}
PopulateEncoders();
}
void DumpingDialog::PopulateEncoders() {
const auto& format = formats.at(ui->formatComboBox->currentData().toUInt());
ui->videoEncoderComboBox->clear();
for (std::size_t i = 0; i < video_encoders.size(); ++i) {
const auto& video_encoder = video_encoders[i];
if (!format.supported_video_codecs.count(video_encoder.codec)) {
continue;
}
ui->videoEncoderComboBox->addItem(
tr("%1 (%2)").arg(QString::fromStdString(video_encoder.long_name),
QString::fromStdString(video_encoder.name)),
static_cast<unsigned long long>(i));
if (video_encoder.name == Settings::values.video_encoder) {
ui->videoEncoderComboBox->setCurrentIndex(ui->videoEncoderComboBox->count() - 1);
}
}
ui->audioEncoderComboBox->clear();
for (std::size_t i = 0; i < audio_encoders.size(); ++i) {
const auto& audio_encoder = audio_encoders[i];
if (!format.supported_audio_codecs.count(audio_encoder.codec)) {
continue;
}
ui->audioEncoderComboBox->addItem(
tr("%1 (%2)").arg(QString::fromStdString(audio_encoder.long_name),
QString::fromStdString(audio_encoder.name)),
static_cast<unsigned long long>(i));
if (audio_encoder.name == Settings::values.audio_encoder) {
ui->audioEncoderComboBox->setCurrentIndex(ui->audioEncoderComboBox->count() - 1);
}
}
}
void DumpingDialog::OnToolButtonClicked() {
const auto& format = formats.at(ui->formatComboBox->currentData().toUInt());
QString extensions;
for (const auto& ext : format.extensions) {
if (!extensions.isEmpty()) {
extensions.append(QLatin1Char{' '});
}
extensions.append(QStringLiteral("*.%1").arg(QString::fromStdString(ext)));
}
const auto path = QFileDialog::getSaveFileName(
this, tr("Select Video Output Path"), last_path,
tr("%1 (%2)").arg(QString::fromStdString(format.long_name), extensions));
if (!path.isEmpty()) {
last_path = QFileInfo(ui->pathLineEdit->text()).path();
ui->pathLineEdit->setText(path);
}
}
void DumpingDialog::OpenOptionsDialog(const std::vector<VideoDumper::OptionInfo>& specific_options,
const std::vector<VideoDumper::OptionInfo>& generic_options,
QLineEdit* line_edit) {
OptionsDialog dialog(this, specific_options, generic_options, line_edit->text().toStdString());
if (dialog.exec() != QDialog::DialogCode::Accepted) {
return;
}
line_edit->setText(QString::fromStdString(dialog.GetCurrentValue()));
}
void DumpingDialog::SetConfiguration() {
Populate();
ui->formatOptionsLineEdit->setText(QString::fromStdString(Settings::values.format_options));
ui->videoEncoderOptionsLineEdit->setText(
QString::fromStdString(Settings::values.video_encoder_options));
ui->audioEncoderOptionsLineEdit->setText(
QString::fromStdString(Settings::values.audio_encoder_options));
last_path = UISettings::values.video_dumping_path;
ui->videoBitrateSpinBox->setValue(static_cast<int>(Settings::values.video_bitrate));
ui->audioBitrateSpinBox->setValue(static_cast<int>(Settings::values.audio_bitrate));
}
void DumpingDialog::ApplyConfiguration() {
Settings::values.output_format = formats.at(ui->formatComboBox->currentData().toUInt()).name;
Settings::values.format_options = ui->formatOptionsLineEdit->text().toStdString();
Settings::values.video_encoder =
video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).name;
Settings::values.video_encoder_options = ui->videoEncoderOptionsLineEdit->text().toStdString();
Settings::values.video_bitrate = ui->videoBitrateSpinBox->value();
Settings::values.audio_encoder =
audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).name;
Settings::values.audio_encoder_options = ui->audioEncoderOptionsLineEdit->text().toStdString();
Settings::values.audio_bitrate = ui->audioBitrateSpinBox->value();
UISettings::values.video_dumping_path = last_path;
Settings::Apply();
}

View File

@@ -0,0 +1,43 @@
// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <memory>
#include <QDialog>
#include "core/dumping/ffmpeg_backend.h"
namespace Ui {
class DumpingDialog;
}
class QLineEdit;
class DumpingDialog : public QDialog {
Q_OBJECT
public:
explicit DumpingDialog(QWidget* parent);
~DumpingDialog() override;
QString GetFilePath() const;
void ApplyConfiguration();
private:
void Populate();
void PopulateEncoders();
void SetConfiguration();
void OnToolButtonClicked();
void OpenOptionsDialog(const std::vector<VideoDumper::OptionInfo>& specific_options,
const std::vector<VideoDumper::OptionInfo>& generic_options,
QLineEdit* line_edit);
std::unique_ptr<Ui::DumpingDialog> ui;
QString last_path;
std::vector<VideoDumper::FormatInfo> formats;
std::vector<VideoDumper::OptionInfo> format_generic_options;
std::vector<VideoDumper::EncoderInfo> video_encoders;
std::vector<VideoDumper::EncoderInfo> audio_encoders;
std::vector<VideoDumper::OptionInfo> encoder_generic_options;
};

View File

@@ -0,0 +1,213 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DumpingDialog</class>
<widget class="QDialog" name="DumpingDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>420</height>
</rect>
</property>
<property name="windowTitle">
<string>Dump Video</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QGroupBox">
<property name="title">
<string>Output</string>
</property>
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="QLabel">
<property name="text">
<string>Format:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="formatComboBox"/>
</item>
<item row="1" column="0">
<widget class="QLabel">
<property name="text">
<string>Options:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="formatOptionsLineEdit"/>
</item>
<item row="1" column="2">
<widget class="QToolButton" name="formatOptionsButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel">
<property name="text">
<string>Path:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="pathLineEdit"/>
</item>
<item row="2" column="2">
<widget class="QToolButton" name="pathExplore">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox">
<property name="title">
<string>Video</string>
</property>
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="QLabel">
<property name="text">
<string>Encoder:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="videoEncoderComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel">
<property name="text">
<string>Options:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="videoEncoderOptionsLineEdit"/>
</item>
<item row="1" column="2">
<widget class="QToolButton" name="videoEncoderOptionsButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel">
<property name="text">
<string>Bitrate:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="videoBitrateSpinBox">
<property name="maximum">
<number>10000000</number>
</property>
<property name="singleStep">
<number>1000</number>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel">
<property name="text">
<string>bps</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox">
<property name="title">
<string>Audio</string>
</property>
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="QLabel">
<property name="text">
<string>Encoder:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="audioEncoderComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel">
<property name="text">
<string>Options:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="audioEncoderOptionsLineEdit"/>
</item>
<item row="1" column="2">
<widget class="QToolButton" name="audioEncoderOptionsButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel">
<property name="text">
<string>Bitrate:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="audioBitrateSpinBox">
<property name="maximum">
<number>1000000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel">
<property name="text">
<string>bps</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
</ui>

View File

@@ -0,0 +1,299 @@
// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <unordered_map>
#include <QCheckBox>
#include <QStringList>
#include "citra_qt/dumping/option_set_dialog.h"
#include "common/logging/log.h"
#include "common/string_util.h"
#include "ui_option_set_dialog.h"
extern "C" {
#include <libavutil/pixdesc.h>
}
static const std::unordered_map<AVOptionType, const char*> TypeNameMap{{
{AV_OPT_TYPE_BOOL, QT_TR_NOOP("boolean")},
{AV_OPT_TYPE_FLAGS, QT_TR_NOOP("flags")},
{AV_OPT_TYPE_DURATION, QT_TR_NOOP("duration")},
{AV_OPT_TYPE_INT, QT_TR_NOOP("int")},
{AV_OPT_TYPE_UINT64, QT_TR_NOOP("uint64")},
{AV_OPT_TYPE_INT64, QT_TR_NOOP("int64")},
{AV_OPT_TYPE_DOUBLE, QT_TR_NOOP("double")},
{AV_OPT_TYPE_FLOAT, QT_TR_NOOP("float")},
{AV_OPT_TYPE_RATIONAL, QT_TR_NOOP("rational")},
{AV_OPT_TYPE_PIXEL_FMT, QT_TR_NOOP("pixel format")},
{AV_OPT_TYPE_SAMPLE_FMT, QT_TR_NOOP("sample format")},
{AV_OPT_TYPE_COLOR, QT_TR_NOOP("color")},
{AV_OPT_TYPE_IMAGE_SIZE, QT_TR_NOOP("image size")},
{AV_OPT_TYPE_STRING, QT_TR_NOOP("string")},
{AV_OPT_TYPE_DICT, QT_TR_NOOP("dictionary")},
{AV_OPT_TYPE_VIDEO_RATE, QT_TR_NOOP("video rate")},
{AV_OPT_TYPE_CHANNEL_LAYOUT, QT_TR_NOOP("channel layout")},
}};
static const std::unordered_map<AVOptionType, const char*> TypeDescriptionMap{{
{AV_OPT_TYPE_DURATION, QT_TR_NOOP("[&lt;hours (integer)>:][&lt;minutes (integer):]&lt;seconds "
"(decimal)> e.g. 03:00.5 (3min 500ms)")},
{AV_OPT_TYPE_RATIONAL, QT_TR_NOOP("&lt;num>/&lt;den>")},
{AV_OPT_TYPE_COLOR, QT_TR_NOOP("0xRRGGBBAA")},
{AV_OPT_TYPE_IMAGE_SIZE, QT_TR_NOOP("&lt;width>x&lt;height>, or preset values like 'vga'.")},
{AV_OPT_TYPE_DICT,
QT_TR_NOOP("Comma-splitted list of &lt;key>=&lt;value>. Do not put spaces.")},
{AV_OPT_TYPE_VIDEO_RATE, QT_TR_NOOP("&lt;num>/&lt;den>, or preset values like 'pal'.")},
{AV_OPT_TYPE_CHANNEL_LAYOUT, QT_TR_NOOP("Hexadecimal channel layout mask starting with '0x'.")},
}};
/// Get the preset values of an option. returns {display value, real value}
std::vector<std::pair<QString, QString>> GetPresetValues(const VideoDumper::OptionInfo& option) {
switch (option.type) {
case AV_OPT_TYPE_BOOL: {
return {{QObject::tr("auto"), QStringLiteral("auto")},
{QObject::tr("true"), QStringLiteral("true")},
{QObject::tr("false"), QStringLiteral("false")}};
}
case AV_OPT_TYPE_PIXEL_FMT: {
std::vector<std::pair<QString, QString>> out{{QObject::tr("none"), QStringLiteral("none")}};
// List all pixel formats
const AVPixFmtDescriptor* current = nullptr;
while ((current = av_pix_fmt_desc_next(current))) {
out.emplace_back(QString::fromUtf8(current->name), QString::fromUtf8(current->name));
}
return out;
}
case AV_OPT_TYPE_SAMPLE_FMT: {
std::vector<std::pair<QString, QString>> out{{QObject::tr("none"), QStringLiteral("none")}};
// List all sample formats
int current = 0;
while (true) {
const char* name = av_get_sample_fmt_name(static_cast<AVSampleFormat>(current));
if (name == nullptr)
break;
out.emplace_back(QString::fromUtf8(name), QString::fromUtf8(name));
}
return out;
}
case AV_OPT_TYPE_INT:
case AV_OPT_TYPE_INT64:
case AV_OPT_TYPE_UINT64: {
std::vector<std::pair<QString, QString>> out;
// Add in all named constants
for (const auto& constant : option.named_constants) {
out.emplace_back(QObject::tr("%1 (0x%2)")
.arg(QString::fromStdString(constant.name))
.arg(constant.value, 0, 16),
QString::fromStdString(constant.name));
}
return out;
}
default:
return {};
}
}
void OptionSetDialog::InitializeUI(const std::string& initial_value) {
const QString type_name =
TypeNameMap.count(option.type) ? tr(TypeNameMap.at(option.type)) : tr("unknown");
ui->nameLabel->setText(tr("%1 &lt;%2> %3")
.arg(QString::fromStdString(option.name), type_name,
QString::fromStdString(option.description)));
if (TypeDescriptionMap.count(option.type)) {
ui->formatLabel->setVisible(true);
ui->formatLabel->setText(tr(TypeDescriptionMap.at(option.type)));
}
if (option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 ||
option.type == AV_OPT_TYPE_UINT64 || option.type == AV_OPT_TYPE_FLOAT ||
option.type == AV_OPT_TYPE_DOUBLE || option.type == AV_OPT_TYPE_DURATION ||
option.type == AV_OPT_TYPE_RATIONAL) { // scalar types
ui->formatLabel->setVisible(true);
if (!ui->formatLabel->text().isEmpty()) {
ui->formatLabel->text().append(QStringLiteral("\n"));
}
ui->formatLabel->setText(
ui->formatLabel->text().append(tr("Range: %1 - %2").arg(option.min).arg(option.max)));
}
// Decide and initialize layout
if (option.type == AV_OPT_TYPE_BOOL || option.type == AV_OPT_TYPE_PIXEL_FMT ||
option.type == AV_OPT_TYPE_SAMPLE_FMT ||
((option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 ||
option.type == AV_OPT_TYPE_UINT64) &&
!option.named_constants.empty())) { // Use the combobox layout
layout_type = 1;
ui->comboBox->setVisible(true);
ui->comboBoxHelpLabel->setVisible(true);
QString real_initial_value = QString::fromStdString(initial_value);
if (option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 ||
option.type == AV_OPT_TYPE_UINT64) {
// Get the name of the initial value
try {
s64 initial_value_integer = std::stoll(initial_value, nullptr, 0);
for (const auto& constant : option.named_constants) {
if (constant.value == initial_value_integer) {
real_initial_value = QString::fromStdString(constant.name);
break;
}
}
} catch (...) {
// Not convertible to integer, ignore
}
}
bool found = false;
for (const auto& [display, value] : GetPresetValues(option)) {
ui->comboBox->addItem(display, value);
if (value == real_initial_value) {
found = true;
ui->comboBox->setCurrentIndex(ui->comboBox->count() - 1);
}
}
ui->comboBox->addItem(tr("custom"));
if (!found) {
ui->comboBox->setCurrentIndex(ui->comboBox->count() - 1);
ui->lineEdit->setText(QString::fromStdString(initial_value));
}
UpdateUIDisplay();
connect(ui->comboBox, &QComboBox::currentTextChanged, this,
&OptionSetDialog::UpdateUIDisplay);
} else if (option.type == AV_OPT_TYPE_FLAGS &&
!option.named_constants.empty()) { // Use the check boxes layout
layout_type = 2;
for (const auto& constant : option.named_constants) {
auto* checkBox = new QCheckBox(tr("%1 (0x%2) %3")
.arg(QString::fromStdString(constant.name))
.arg(constant.value, 0, 16)
.arg(QString::fromStdString(constant.description)));
checkBox->setProperty("value", static_cast<unsigned long long>(constant.value));
checkBox->setProperty("name", QString::fromStdString(constant.name));
ui->checkBoxLayout->addWidget(checkBox);
}
SetCheckBoxDefaults(initial_value);
} else { // Use the line edit layout
layout_type = 0;
ui->lineEdit->setVisible(true);
ui->lineEdit->setText(QString::fromStdString(initial_value));
}
adjustSize();
}
void OptionSetDialog::SetCheckBoxDefaults(const std::string& initial_value) {
if (initial_value.size() >= 2 &&
(initial_value.substr(0, 2) == "0x" || initial_value.substr(0, 2) == "0X")) {
// This is a hex mask
try {
u64 value = std::stoull(initial_value, nullptr, 16);
for (int i = 0; i < ui->checkBoxLayout->count(); ++i) {
auto* checkBox = qobject_cast<QCheckBox*>(ui->checkBoxLayout->itemAt(i)->widget());
if (checkBox) {
checkBox->setChecked(value & checkBox->property("value").toULongLong());
}
}
} catch (...) {
LOG_ERROR(Frontend, "Could not convert {} to number", initial_value);
}
} else {
// This is a combination of constants, splitted with + or |
std::vector<std::string> tmp;
Common::SplitString(initial_value, '+', tmp);
std::vector<std::string> out;
std::vector<std::string> tmp2;
for (const auto& str : tmp) {
Common::SplitString(str, '|', tmp2);
out.insert(out.end(), tmp2.begin(), tmp2.end());
}
for (int i = 0; i < ui->checkBoxLayout->count(); ++i) {
auto* checkBox = qobject_cast<QCheckBox*>(ui->checkBoxLayout->itemAt(i)->widget());
if (checkBox) {
checkBox->setChecked(
std::find(out.begin(), out.end(),
checkBox->property("name").toString().toStdString()) != out.end());
}
}
}
}
void OptionSetDialog::UpdateUIDisplay() {
if (layout_type != 1)
return;
if (ui->comboBox->currentIndex() == ui->comboBox->count() - 1) { // custom
ui->comboBoxHelpLabel->setVisible(false);
ui->lineEdit->setVisible(true);
adjustSize();
return;
}
ui->lineEdit->setVisible(false);
for (const auto& constant : option.named_constants) {
if (constant.name == ui->comboBox->currentData().toString().toStdString()) {
ui->comboBoxHelpLabel->setVisible(true);
ui->comboBoxHelpLabel->setText(QString::fromStdString(constant.description));
return;
}
}
}
std::pair<bool, std::string> OptionSetDialog::GetCurrentValue() {
if (!is_set) {
return {};
}
switch (layout_type) {
case 0: // line edit layout
return {true, ui->lineEdit->text().toStdString()};
case 1: // combo box layout
if (ui->comboBox->currentIndex() == ui->comboBox->count() - 1) {
return {true, ui->lineEdit->text().toStdString()}; // custom
}
return {true, ui->comboBox->currentData().toString().toStdString()};
case 2: { // check boxes layout
std::string out;
for (int i = 0; i < ui->checkBoxLayout->count(); ++i) {
auto* checkBox = qobject_cast<QCheckBox*>(ui->checkBoxLayout->itemAt(i)->widget());
if (checkBox && checkBox->isChecked()) {
if (!out.empty()) {
out.append("+");
}
out.append(checkBox->property("name").toString().toStdString());
}
}
if (out.empty()) {
out = "0x0";
}
return {true, out};
}
default:
return {};
}
}
OptionSetDialog::OptionSetDialog(QWidget* parent, VideoDumper::OptionInfo option_,
const std::string& initial_value)
: QDialog(parent), ui(std::make_unique<Ui::OptionSetDialog>()), option(std::move(option_)) {
ui->setupUi(this);
InitializeUI(initial_value);
connect(ui->unsetButton, &QPushButton::clicked, [this] {
is_set = false;
accept();
});
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &OptionSetDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &OptionSetDialog::reject);
}
OptionSetDialog::~OptionSetDialog() = default;

View File

@@ -0,0 +1,33 @@
// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <memory>
#include <QDialog>
#include "core/dumping/ffmpeg_backend.h"
namespace Ui {
class OptionSetDialog;
}
class OptionSetDialog : public QDialog {
Q_OBJECT
public:
explicit OptionSetDialog(QWidget* parent, VideoDumper::OptionInfo option,
const std::string& initial_value);
~OptionSetDialog() override;
// {is_set, value}
std::pair<bool, std::string> GetCurrentValue();
private:
void InitializeUI(const std::string& initial_value);
void SetCheckBoxDefaults(const std::string& initial_value);
void UpdateUIDisplay();
std::unique_ptr<Ui::OptionSetDialog> ui;
VideoDumper::OptionInfo option;
bool is_set = true;
int layout_type = -1; // 0 - line edit, 1 - combo box, 2 - flags (check boxes)
};

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OptionSetDialog</class>
<widget class="QDialog" name="OptionSetDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>150</height>
</rect>
</property>
<property name="windowTitle">
<string>Options</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="nameLabel"/>
</item>
<item>
<widget class="QLabel" name="formatLabel">
<property name="visible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="comboBoxLayout">
<item>
<widget class="QComboBox" name="comboBox">
<property name="visible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="comboBoxHelpLabel">
<property name="visible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLineEdit" name="lineEdit">
<property name="visible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="checkBoxLayout"/>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QPushButton" name="unsetButton">
<property name="text">
<string>Unset</string>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</ui>

View File

@@ -0,0 +1,68 @@
// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <QTreeWidgetItem>
#include "citra_qt/dumping/option_set_dialog.h"
#include "citra_qt/dumping/options_dialog.h"
#include "ui_options_dialog.h"
constexpr char UNSET_TEXT[] = QT_TR_NOOP("[not set]");
void OptionsDialog::PopulateOptions() {
const auto& options = ui->specificRadioButton->isChecked() ? specific_options : generic_options;
ui->main->clear();
ui->main->setSortingEnabled(false);
for (std::size_t i = 0; i < options.size(); ++i) {
const auto& option = options.at(i);
auto* item = new QTreeWidgetItem(
{QString::fromStdString(option.name), QString::fromStdString(current_values.Get(
option.name, tr(UNSET_TEXT).toStdString()))});
item->setData(1, Qt::UserRole, static_cast<unsigned long long>(i)); // ID
ui->main->addTopLevelItem(item);
}
ui->main->setSortingEnabled(true);
ui->main->sortItems(0, Qt::AscendingOrder);
}
void OptionsDialog::OnSetOptionValue(QTreeWidgetItem* item) {
const auto& options = ui->specificRadioButton->isChecked() ? specific_options : generic_options;
const int id = item->data(1, Qt::UserRole).toInt();
OptionSetDialog dialog(this, options[id],
current_values.Get(options[id].name, options[id].default_value));
if (dialog.exec() != QDialog::DialogCode::Accepted) {
return;
}
const auto& [is_set, value] = dialog.GetCurrentValue();
if (is_set) {
current_values.Set(options[id].name, value);
} else {
current_values.Erase(options[id].name);
}
item->setText(1, is_set ? QString::fromStdString(value) : tr(UNSET_TEXT));
}
std::string OptionsDialog::GetCurrentValue() const {
return current_values.Serialize();
}
OptionsDialog::OptionsDialog(QWidget* parent,
std::vector<VideoDumper::OptionInfo> specific_options_,
std::vector<VideoDumper::OptionInfo> generic_options_,
const std::string& current_value)
: QDialog(parent), ui(std::make_unique<Ui::OptionsDialog>()),
specific_options(std::move(specific_options_)), generic_options(std::move(generic_options_)),
current_values(current_value) {
ui->setupUi(this);
PopulateOptions();
connect(ui->main, &QTreeWidget::itemDoubleClicked,
[this](QTreeWidgetItem* item, int column) { OnSetOptionValue(item); });
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &OptionsDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &OptionsDialog::reject);
connect(ui->specificRadioButton, &QRadioButton::toggled, this, &OptionsDialog::PopulateOptions);
}
OptionsDialog::~OptionsDialog() = default;

View File

@@ -0,0 +1,36 @@
// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <memory>
#include <vector>
#include <QDialog>
#include "common/param_package.h"
#include "core/dumping/ffmpeg_backend.h"
class QTreeWidgetItem;
namespace Ui {
class OptionsDialog;
}
class OptionsDialog : public QDialog {
Q_OBJECT
public:
explicit OptionsDialog(QWidget* parent, std::vector<VideoDumper::OptionInfo> specific_options,
std::vector<VideoDumper::OptionInfo> generic_options,
const std::string& current_value);
~OptionsDialog() override;
std::string GetCurrentValue() const;
private:
void PopulateOptions();
void OnSetOptionValue(QTreeWidgetItem* item);
std::unique_ptr<Ui::OptionsDialog> ui;
std::vector<VideoDumper::OptionInfo> specific_options;
std::vector<VideoDumper::OptionInfo> generic_options;
Common::ParamPackage current_values;
};

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OptionsDialog</class>
<widget class="QDialog" name="OptionsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>650</width>
<height>350</height>
</rect>
</property>
<property name="windowTitle">
<string>Options</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLabel">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="text">
<string>Double click to see the description and change the values of the options.</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QRadioButton" name="specificRadioButton">
<property name="text">
<string>Specific</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="genericRadioButton">
<property name="text">
<string>Generic</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTreeWidget" name="main">
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
</ui>

View File

@@ -87,6 +87,10 @@
#include "citra_qt/discord_impl.h"
#endif
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
#include "citra_qt/dumping/dumping_dialog.h"
#endif
#ifdef QT_STATICPLUGIN
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
#endif
@@ -679,9 +683,7 @@ void GMainWindow::ConnectMenuEvents() {
connect(ui.action_Capture_Screenshot, &QAction::triggered, this,
&GMainWindow::OnCaptureScreenshot);
#ifndef ENABLE_FFMPEG_VIDEO_DUMPER
ui.action_Dump_Video->setEnabled(false);
#endif
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
connect(ui.action_Dump_Video, &QAction::triggered, [this] {
if (ui.action_Dump_Video->isChecked()) {
OnStartVideoDumping();
@@ -689,6 +691,9 @@ void GMainWindow::ConnectMenuEvents() {
OnStopVideoDumping();
}
});
#else
ui.action_Dump_Video->setEnabled(false);
#endif
// Help
connect(ui.action_Open_Citra_Folder, &QAction::triggered, this,
@@ -975,8 +980,14 @@ void GMainWindow::BootGame(const QString& filename) {
if (video_dumping_on_start) {
Layout::FramebufferLayout layout{
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
Core::System::GetInstance().VideoDumper().StartDumping(video_dumping_path.toStdString(),
"webm", layout);
if (!Core::System::GetInstance().VideoDumper().StartDumping(
video_dumping_path.toStdString(), layout)) {
QMessageBox::critical(
this, tr("Citra"),
tr("Could not start video dumping.<br>Refer to the log for details."));
ui.action_Dump_Video->setChecked(false);
}
video_dumping_on_start = false;
video_dumping_path.clear();
}
@@ -992,11 +1003,13 @@ void GMainWindow::ShutdownGame() {
HideFullscreen();
}
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
if (Core::System::GetInstance().VideoDumper().IsDumping()) {
game_shutdown_delayed = true;
OnStopVideoDumping();
return;
}
#endif
AllowOSSleep();
@@ -1804,18 +1817,23 @@ void GMainWindow::OnCaptureScreenshot() {
OnStartGame();
}
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
void GMainWindow::OnStartVideoDumping() {
const QString path = QFileDialog::getSaveFileName(
this, tr("Save Video"), UISettings::values.video_dumping_path, tr("WebM Videos (*.webm)"));
if (path.isEmpty()) {
DumpingDialog dialog(this);
if (dialog.exec() != QDialog::DialogCode::Accepted) {
ui.action_Dump_Video->setChecked(false);
return;
}
UISettings::values.video_dumping_path = QFileInfo(path).path();
const auto path = dialog.GetFilePath();
if (emulation_running) {
Layout::FramebufferLayout layout{
Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())};
Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), "webm", layout);
if (!Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), layout)) {
QMessageBox::critical(
this, tr("Citra"),
tr("Could not start video dumping.<br>Refer to the log for details."));
ui.action_Dump_Video->setChecked(false);
}
} else {
video_dumping_on_start = true;
video_dumping_path = path;
@@ -1832,6 +1850,8 @@ void GMainWindow::OnStopVideoDumping() {
const bool was_dumping = Core::System::GetInstance().VideoDumper().IsDumping();
if (!was_dumping)
return;
game_paused_for_dumping = emu_thread->IsRunning();
OnPauseGame();
auto future =
@@ -1841,13 +1861,15 @@ void GMainWindow::OnStopVideoDumping() {
if (game_shutdown_delayed) {
game_shutdown_delayed = false;
ShutdownGame();
} else {
} else if (game_paused_for_dumping) {
game_paused_for_dumping = false;
OnStartGame();
}
});
future_watcher->setFuture(future);
}
}
#endif
void GMainWindow::UpdateStatusBar() {
if (emu_thread == nullptr) {

View File

@@ -200,8 +200,10 @@ private slots:
void OnPlayMovie();
void OnStopRecordingPlayback();
void OnCaptureScreenshot();
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
void OnStartVideoDumping();
void OnStopVideoDumping();
#endif
void OnCoreError(Core::System::ResultStatus, std::string);
/// Called whenever a user selects Help->About Citra
void OnMenuAboutCitra();
@@ -256,6 +258,8 @@ private:
QString video_dumping_path;
// Whether game shutdown is delayed due to video dumping
bool game_shutdown_delayed = false;
// Whether game was paused due to stopping video dumping
bool game_paused_for_dumping = false;
// Debugger panes
ProfilerWidget* profilerWidget;