797b3dc121
This changes the remux dialog to support a collection of input/output pairs, presented as a QTableView. Standard Qt icons are used to indicate the state of each entry during a remux operation. Drag/drop support is added to populate the list quickly. Both Dark and Rachni themes are updated to make QTableView look reasonable. Relevant text is added in the localization files. Closes obsproject/obs-studio#1153
978 lines
24 KiB
C++
978 lines
24 KiB
C++
/******************************************************************************
|
|
Copyright (C) 2014 by Ruwen Hahn <palana@stunned.de>
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
******************************************************************************/
|
|
|
|
#include "window-remux.hpp"
|
|
|
|
#include "obs-app.hpp"
|
|
|
|
#include <QCloseEvent>
|
|
#include <QDirIterator>
|
|
#include <QFileDialog>
|
|
#include <QItemDelegate>
|
|
#include <QLineEdit>
|
|
#include <QMessageBox>
|
|
#include <QMimeData>
|
|
#include <QPainter>
|
|
#include <QPushButton>
|
|
#include <QStandardItemModel>
|
|
#include <QStyledItemDelegate>
|
|
#include <QToolButton>
|
|
|
|
#include "qt-wrappers.hpp"
|
|
|
|
#include <memory>
|
|
#include <cmath>
|
|
|
|
using namespace std;
|
|
|
|
enum RemuxEntryColumn {
|
|
State,
|
|
InputPath,
|
|
OutputPath,
|
|
|
|
Count
|
|
};
|
|
|
|
enum RemuxEntryRole {
|
|
EntryStateRole = Qt::UserRole,
|
|
NewPathsToProcessRole
|
|
};
|
|
|
|
/**********************************************************
|
|
Delegate - Presents cells in the grid.
|
|
**********************************************************/
|
|
|
|
class RemuxEntryPathItemDelegate : public QStyledItemDelegate {
|
|
public:
|
|
|
|
RemuxEntryPathItemDelegate(bool isOutput, const QString &defaultPath)
|
|
: QStyledItemDelegate(),
|
|
isOutput(isOutput),
|
|
defaultPath(defaultPath)
|
|
{
|
|
}
|
|
|
|
virtual QWidget *createEditor(QWidget *parent,
|
|
const QStyleOptionViewItem & /* option */,
|
|
const QModelIndex &index) const override
|
|
{
|
|
RemuxEntryState state = index.model()
|
|
->index(index.row(), RemuxEntryColumn::State)
|
|
.data(RemuxEntryRole::EntryStateRole)
|
|
.value<RemuxEntryState>();
|
|
if (state == RemuxEntryState::Pending ||
|
|
state == RemuxEntryState::InProgress) {
|
|
// Never allow modification of rows that are
|
|
// in progress.
|
|
return Q_NULLPTR;
|
|
} else if (isOutput && state != RemuxEntryState::Ready) {
|
|
// Do not allow modification of output rows
|
|
// that aren't associated with a valid input.
|
|
return Q_NULLPTR;
|
|
} else if (!isOutput && state == RemuxEntryState::Complete) {
|
|
// Don't allow modification of rows that are
|
|
// already complete.
|
|
return Q_NULLPTR;
|
|
} else {
|
|
QSizePolicy buttonSizePolicy(
|
|
QSizePolicy::Policy::Minimum,
|
|
QSizePolicy::Policy::Expanding,
|
|
QSizePolicy::ControlType::PushButton);
|
|
|
|
QWidget *container = new QWidget(parent);
|
|
|
|
auto browseCallback = [this, container]()
|
|
{
|
|
const_cast<RemuxEntryPathItemDelegate *>(this)
|
|
->handleBrowse(container);
|
|
};
|
|
|
|
auto clearCallback = [this, container]()
|
|
{
|
|
const_cast<RemuxEntryPathItemDelegate *>(this)
|
|
->handleClear(container);
|
|
};
|
|
|
|
QHBoxLayout *layout = new QHBoxLayout();
|
|
layout->setMargin(0);
|
|
layout->setSpacing(0);
|
|
|
|
QLineEdit *text = new QLineEdit();
|
|
text->setObjectName(QStringLiteral("text"));
|
|
text->setSizePolicy(QSizePolicy(
|
|
QSizePolicy::Policy::Expanding,
|
|
QSizePolicy::Policy::Expanding,
|
|
QSizePolicy::ControlType::LineEdit));
|
|
layout->addWidget(text);
|
|
|
|
QToolButton *browseButton = new QToolButton();
|
|
browseButton->setText("...");
|
|
browseButton->setSizePolicy(buttonSizePolicy);
|
|
layout->addWidget(browseButton);
|
|
|
|
container->connect(browseButton, &QToolButton::clicked,
|
|
browseCallback);
|
|
|
|
// The "clear" button is not shown in output cells
|
|
// or the insertion point's input cell.
|
|
if (!isOutput && state != RemuxEntryState::Empty) {
|
|
QToolButton *clearButton = new QToolButton();
|
|
clearButton->setText("X");
|
|
clearButton->setSizePolicy(buttonSizePolicy);
|
|
layout->addWidget(clearButton);
|
|
|
|
container->connect(clearButton,
|
|
&QToolButton::clicked,
|
|
clearCallback);
|
|
}
|
|
|
|
container->setLayout(layout);
|
|
container->setFocusProxy(text);
|
|
|
|
return container;
|
|
}
|
|
}
|
|
|
|
virtual void setEditorData(QWidget *editor, const QModelIndex &index)
|
|
const override
|
|
{
|
|
QLineEdit *text = editor->findChild<QLineEdit *>();
|
|
text->setText(index.data().toString());
|
|
|
|
editor->setProperty(PATH_LIST_PROP, QVariant());
|
|
}
|
|
|
|
virtual void setModelData(QWidget *editor,
|
|
QAbstractItemModel *model,
|
|
const QModelIndex &index) const override
|
|
{
|
|
// We use the PATH_LIST_PROP property to pass a list of
|
|
// path strings from the editor widget into the model's
|
|
// NewPathsToProcessRole. This is only used when paths
|
|
// are selected through the "browse" or "delete" buttons
|
|
// in the editor. If the user enters new text in the
|
|
// text box, we simply pass that text on to the model
|
|
// as normal text data in the default role.
|
|
QVariant pathListProp = editor->property(PATH_LIST_PROP);
|
|
if (pathListProp.isValid()) {
|
|
QStringList list = editor->property(PATH_LIST_PROP)
|
|
.toStringList();
|
|
if (isOutput) {
|
|
if (list.size() > 0)
|
|
model->setData(index, list);
|
|
} else
|
|
model->setData(index, list,
|
|
RemuxEntryRole::NewPathsToProcessRole);
|
|
} else {
|
|
QLineEdit *lineEdit = editor->findChild<QLineEdit *>();
|
|
model->setData(index, lineEdit->text());
|
|
}
|
|
}
|
|
|
|
virtual void paint(QPainter *painter,
|
|
const QStyleOptionViewItem &option,
|
|
const QModelIndex &index) const override
|
|
{
|
|
RemuxEntryState state = index.model()
|
|
->index(index.row(), RemuxEntryColumn::State)
|
|
.data(RemuxEntryRole::EntryStateRole)
|
|
.value<RemuxEntryState>();
|
|
|
|
QStyleOptionViewItem localOption = option;
|
|
initStyleOption(&localOption, index);
|
|
|
|
if (isOutput) {
|
|
if (state != Ready) {
|
|
QColor background = localOption.palette
|
|
.color(QPalette::ColorGroup::Disabled,
|
|
QPalette::ColorRole::Background);
|
|
|
|
localOption.backgroundBrush = QBrush(background);
|
|
}
|
|
}
|
|
|
|
QApplication::style()->drawControl(QStyle::CE_ItemViewItem,
|
|
&localOption, painter);
|
|
}
|
|
|
|
private:
|
|
bool isOutput;
|
|
QString defaultPath;
|
|
|
|
const char *PATH_LIST_PROP = "pathList";
|
|
|
|
void handleBrowse(QWidget *container)
|
|
{
|
|
QString OutputPattern =
|
|
"(*.mp4 *.flv *.mov *.mkv *.ts *.m3u8)";
|
|
QString InputPattern =
|
|
"(*.flv *.mov *.mkv *.ts *.m3u8)";
|
|
|
|
QLineEdit *text = container->findChild<QLineEdit *>();
|
|
|
|
QString currentPath = text->text();
|
|
if (currentPath.isEmpty())
|
|
currentPath = defaultPath;
|
|
|
|
bool isSet = false;
|
|
if (isOutput) {
|
|
QString newPath = QFileDialog::getSaveFileName(
|
|
container, QTStr("Remux.SelectTarget"),
|
|
currentPath, OutputPattern);
|
|
|
|
if (!newPath.isEmpty()) {
|
|
container->setProperty(PATH_LIST_PROP,
|
|
QStringList() << newPath);
|
|
isSet = true;
|
|
}
|
|
} else {
|
|
QStringList paths = QFileDialog::getOpenFileNames(
|
|
container,
|
|
QTStr("Remux.SelectRecording"),
|
|
currentPath,
|
|
QTStr("Remux.OBSRecording")
|
|
+ QString(" ") + InputPattern);
|
|
|
|
if (!paths.empty()) {
|
|
container->setProperty(PATH_LIST_PROP, paths);
|
|
isSet = true;
|
|
}
|
|
}
|
|
|
|
if (isSet)
|
|
emit commitData(container);
|
|
}
|
|
|
|
void handleClear(QWidget *container)
|
|
{
|
|
// An empty string list will indicate that the entry is being
|
|
// blanked and should be deleted.
|
|
container->setProperty(PATH_LIST_PROP, QStringList());
|
|
|
|
emit commitData(container);
|
|
}
|
|
};
|
|
|
|
/**********************************************************
|
|
Model - Manages the queue's data
|
|
**********************************************************/
|
|
|
|
int RemuxQueueModel::rowCount(const QModelIndex &) const
|
|
{
|
|
return queue.length() + (isProcessing ? 0 : 1);
|
|
}
|
|
|
|
int RemuxQueueModel::columnCount(const QModelIndex &) const
|
|
{
|
|
return RemuxEntryColumn::Count;
|
|
}
|
|
|
|
QVariant RemuxQueueModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
QVariant result = QVariant();
|
|
|
|
if (index.row() >= queue.length()) {
|
|
return QVariant();
|
|
} else if (role == Qt::DisplayRole) {
|
|
switch (index.column()) {
|
|
case RemuxEntryColumn::InputPath:
|
|
result = queue[index.row()].sourcePath;
|
|
break;
|
|
case RemuxEntryColumn::OutputPath:
|
|
result = queue[index.row()].targetPath;
|
|
break;
|
|
}
|
|
} else if (role == Qt::DecorationRole &&
|
|
index.column() == RemuxEntryColumn::State) {
|
|
result = getIcon(queue[index.row()].state);
|
|
} else if (role == RemuxEntryRole::EntryStateRole) {
|
|
result = queue[index.row()].state;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
QVariant RemuxQueueModel::headerData(int section, Qt::Orientation orientation,
|
|
int role) const
|
|
{
|
|
QVariant result = QVariant();
|
|
|
|
if (role == Qt::DisplayRole &&
|
|
orientation == Qt::Orientation::Horizontal) {
|
|
switch (section) {
|
|
case RemuxEntryColumn::State:
|
|
result = QString();
|
|
break;
|
|
case RemuxEntryColumn::InputPath:
|
|
result = QTStr("Remux.SourceFile");
|
|
break;
|
|
case RemuxEntryColumn::OutputPath:
|
|
result = QTStr("Remux.TargetFile");
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Qt::ItemFlags RemuxQueueModel::flags(const QModelIndex &index) const
|
|
{
|
|
Qt::ItemFlags flags = QAbstractTableModel::flags(index);
|
|
|
|
if (index.column() == RemuxEntryColumn::InputPath) {
|
|
flags |= Qt::ItemIsEditable;
|
|
} else if (index.column() == RemuxEntryColumn::OutputPath &&
|
|
index.row() != queue.length()) {
|
|
flags |= Qt::ItemIsEditable;
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
bool RemuxQueueModel::setData(const QModelIndex &index, const QVariant &value,
|
|
int role)
|
|
{
|
|
bool success = false;
|
|
|
|
if (role == RemuxEntryRole::NewPathsToProcessRole) {
|
|
QStringList pathList = value.toStringList();
|
|
|
|
if (pathList.size() == 0) {
|
|
if (index.row() < queue.size()) {
|
|
beginRemoveRows(QModelIndex(), index.row(),
|
|
index.row());
|
|
queue.removeAt(index.row());
|
|
endRemoveRows();
|
|
}
|
|
} else {
|
|
if (pathList.size() > 1 && index.row() < queue.length()) {
|
|
queue[index.row()].sourcePath = pathList[0];
|
|
checkInputPath(index.row());
|
|
|
|
pathList.removeAt(0);
|
|
|
|
success = true;
|
|
}
|
|
|
|
if (pathList.size() > 0) {
|
|
int row = index.row();
|
|
int lastRow = row + pathList.size() - 1;
|
|
beginInsertRows(QModelIndex(), row, lastRow);
|
|
|
|
for (QString path : pathList) {
|
|
RemuxQueueEntry entry;
|
|
entry.sourcePath = path;
|
|
|
|
queue.insert(row, entry);
|
|
row++;
|
|
}
|
|
endInsertRows();
|
|
|
|
for (row = index.row(); row <= lastRow; row++) {
|
|
checkInputPath(row);
|
|
}
|
|
|
|
success = true;
|
|
}
|
|
}
|
|
} else if (index.row() == queue.length()) {
|
|
QString path = value.toString();
|
|
|
|
if (!path.isEmpty()) {
|
|
RemuxQueueEntry entry;
|
|
entry.sourcePath = path;
|
|
|
|
beginInsertRows(QModelIndex(), queue.length() + 1,
|
|
queue.length() + 1);
|
|
queue.append(entry);
|
|
endInsertRows();
|
|
|
|
checkInputPath(index.row());
|
|
success = true;
|
|
}
|
|
} else {
|
|
QString path = value.toString();
|
|
|
|
if (path.isEmpty()) {
|
|
if (index.column() == RemuxEntryColumn::InputPath) {
|
|
beginRemoveRows(QModelIndex(), index.row(),
|
|
index.row());
|
|
queue.removeAt(index.row());
|
|
endRemoveRows();
|
|
}
|
|
} else {
|
|
switch (index.column()) {
|
|
case RemuxEntryColumn::InputPath:
|
|
queue[index.row()].sourcePath = value.toString();
|
|
checkInputPath(index.row());
|
|
success = true;
|
|
break;
|
|
case RemuxEntryColumn::OutputPath:
|
|
queue[index.row()].targetPath = value.toString();
|
|
emit dataChanged(index, index);
|
|
success = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
QVariant RemuxQueueModel::getIcon(RemuxEntryState state)
|
|
{
|
|
QVariant icon;
|
|
QStyle *style = QApplication::style();
|
|
|
|
switch (state) {
|
|
case RemuxEntryState::Complete:
|
|
icon = style->standardIcon(
|
|
QStyle::SP_DialogApplyButton);
|
|
break;
|
|
|
|
case RemuxEntryState::InProgress:
|
|
icon = style->standardIcon(
|
|
QStyle::SP_ArrowRight);
|
|
break;
|
|
|
|
case RemuxEntryState::Error:
|
|
icon = style->standardIcon(
|
|
QStyle::SP_DialogCancelButton);
|
|
break;
|
|
|
|
case RemuxEntryState::InvalidPath:
|
|
icon = style->standardIcon(
|
|
QStyle::SP_MessageBoxWarning);
|
|
break;
|
|
}
|
|
|
|
return icon;
|
|
}
|
|
|
|
void RemuxQueueModel::checkInputPath(int row)
|
|
{
|
|
RemuxQueueEntry &entry = queue[row];
|
|
|
|
if (entry.sourcePath.isEmpty()) {
|
|
entry.state = RemuxEntryState::Empty;
|
|
} else {
|
|
QFileInfo fileInfo(entry.sourcePath);
|
|
if (fileInfo.exists())
|
|
entry.state = RemuxEntryState::Ready;
|
|
else
|
|
entry.state = RemuxEntryState::InvalidPath;
|
|
|
|
if (entry.state == RemuxEntryState::Ready)
|
|
entry.targetPath = fileInfo.path() + QDir::separator()
|
|
+ fileInfo.baseName() + ".mp4";
|
|
}
|
|
|
|
if (entry.state == RemuxEntryState::Ready && isProcessing)
|
|
entry.state = RemuxEntryState::Pending;
|
|
|
|
emit dataChanged(index(row, 0), index(row, RemuxEntryColumn::Count));
|
|
}
|
|
|
|
QFileInfoList RemuxQueueModel::checkForOverwrites() const
|
|
{
|
|
QFileInfoList list;
|
|
|
|
for (const RemuxQueueEntry &entry : queue) {
|
|
if (entry.state == RemuxEntryState::Ready) {
|
|
QFileInfo fileInfo(entry.targetPath);
|
|
if (fileInfo.exists()) {
|
|
list.append(fileInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
bool RemuxQueueModel::checkForErrors() const
|
|
{
|
|
bool hasErrors = false;
|
|
|
|
for (const RemuxQueueEntry &entry : queue) {
|
|
if (entry.state == RemuxEntryState::Error) {
|
|
hasErrors = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return hasErrors;
|
|
}
|
|
|
|
void RemuxQueueModel::clearAll()
|
|
{
|
|
beginRemoveRows(QModelIndex(), 0, queue.size() - 1);
|
|
queue.clear();
|
|
endRemoveRows();
|
|
}
|
|
|
|
void RemuxQueueModel::clearFinished()
|
|
{
|
|
int index = 0;
|
|
|
|
for (index = 0; index < queue.size(); index++) {
|
|
const RemuxQueueEntry &entry = queue[index];
|
|
if (entry.state == RemuxEntryState::Complete) {
|
|
beginRemoveRows(QModelIndex(), index, index);
|
|
queue.removeAt(index);
|
|
endRemoveRows();
|
|
index--;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool RemuxQueueModel::canClearFinished() const
|
|
{
|
|
bool canClearFinished = false;
|
|
for (const RemuxQueueEntry &entry : queue)
|
|
if (entry.state == RemuxEntryState::Complete) {
|
|
canClearFinished = true;
|
|
break;
|
|
}
|
|
|
|
return canClearFinished;
|
|
}
|
|
|
|
void RemuxQueueModel::beginProcessing()
|
|
{
|
|
for (RemuxQueueEntry &entry : queue)
|
|
if (entry.state == RemuxEntryState::Ready)
|
|
entry.state = RemuxEntryState::Pending;
|
|
|
|
// Signal that the insertion point no longer exists.
|
|
beginRemoveRows(QModelIndex(), queue.length(), queue.length());
|
|
endRemoveRows();
|
|
|
|
isProcessing = true;
|
|
|
|
emit dataChanged(index(0, RemuxEntryColumn::State),
|
|
index(queue.length(), RemuxEntryColumn::State));
|
|
}
|
|
|
|
void RemuxQueueModel::endProcessing()
|
|
{
|
|
for (RemuxQueueEntry &entry : queue) {
|
|
if (entry.state == RemuxEntryState::Pending) {
|
|
entry.state = RemuxEntryState::Ready;
|
|
}
|
|
}
|
|
|
|
// Signal that the insertion point exists again.
|
|
beginInsertRows(QModelIndex(), queue.length(), queue.length());
|
|
endInsertRows();
|
|
|
|
isProcessing = false;
|
|
|
|
emit dataChanged(index(0, RemuxEntryColumn::State),
|
|
index(queue.length(), RemuxEntryColumn::State));
|
|
}
|
|
|
|
bool RemuxQueueModel::beginNextEntry(QString &inputPath, QString &outputPath)
|
|
{
|
|
bool anyStarted = false;
|
|
|
|
for (int row = 0; row < queue.length(); row++) {
|
|
RemuxQueueEntry &entry = queue[row];
|
|
if (entry.state == RemuxEntryState::Pending) {
|
|
entry.state = RemuxEntryState::InProgress;
|
|
|
|
inputPath = entry.sourcePath;
|
|
outputPath = entry.targetPath;
|
|
|
|
QModelIndex index = this->index(row,
|
|
RemuxEntryColumn::State);
|
|
emit dataChanged(index, index);
|
|
|
|
anyStarted = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return anyStarted;
|
|
}
|
|
|
|
void RemuxQueueModel::finishEntry(bool success)
|
|
{
|
|
for (int row = 0; row < queue.length(); row++) {
|
|
RemuxQueueEntry &entry = queue[row];
|
|
if (entry.state == RemuxEntryState::InProgress) {
|
|
if (success)
|
|
entry.state = RemuxEntryState::Complete;
|
|
else
|
|
entry.state = RemuxEntryState::Error;
|
|
|
|
QModelIndex index = this->index(row,
|
|
RemuxEntryColumn::State);
|
|
emit dataChanged(index, index);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**********************************************************
|
|
The actual remux window implementation
|
|
**********************************************************/
|
|
|
|
OBSRemux::OBSRemux(const char *path, QWidget *parent)
|
|
: QDialog (parent),
|
|
queueModel(new RemuxQueueModel),
|
|
worker (new RemuxWorker()),
|
|
ui (new Ui::OBSRemux),
|
|
recPath (path)
|
|
{
|
|
setAcceptDrops(true);
|
|
|
|
ui->setupUi(this);
|
|
|
|
ui->progressBar->setVisible(false);
|
|
ui->buttonBox->button(QDialogButtonBox::Ok)->
|
|
setEnabled(false);
|
|
ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->
|
|
setEnabled(false);
|
|
|
|
ui->progressBar->setMinimum(0);
|
|
ui->progressBar->setMaximum(1000);
|
|
ui->progressBar->setValue(0);
|
|
|
|
ui->tableView->setModel(queueModel);
|
|
ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::InputPath,
|
|
new RemuxEntryPathItemDelegate(false, recPath));
|
|
ui->tableView->setItemDelegateForColumn(RemuxEntryColumn::OutputPath,
|
|
new RemuxEntryPathItemDelegate(true, recPath));
|
|
ui->tableView->horizontalHeader()->setSectionResizeMode(
|
|
QHeaderView::ResizeMode::Stretch);
|
|
ui->tableView->horizontalHeader()->setSectionResizeMode(
|
|
RemuxEntryColumn::State,
|
|
QHeaderView::ResizeMode::Fixed);
|
|
ui->tableView->setEditTriggers(
|
|
QAbstractItemView::EditTrigger::CurrentChanged);
|
|
|
|
installEventFilter(CreateShortcutFilter());
|
|
|
|
ui->buttonBox->button(QDialogButtonBox::Ok)->
|
|
setText(QTStr("Remux.Remux"));
|
|
ui->buttonBox->button(QDialogButtonBox::Reset)->
|
|
setText(QTStr("Remux.ClearFinished"));
|
|
ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->
|
|
setText(QTStr("Remux.ClearAll"));
|
|
ui->buttonBox->button(QDialogButtonBox::Reset)->
|
|
setDisabled(true);
|
|
|
|
connect(ui->buttonBox->button(QDialogButtonBox::Ok),
|
|
SIGNAL(clicked()), this, SLOT(beginRemux()));
|
|
connect(ui->buttonBox->button(QDialogButtonBox::Reset),
|
|
SIGNAL(clicked()), this, SLOT(clearFinished()));
|
|
connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults),
|
|
SIGNAL(clicked()), this, SLOT(clearAll()));
|
|
connect(ui->buttonBox->button(QDialogButtonBox::Close),
|
|
SIGNAL(clicked()), this, SLOT(close()));
|
|
|
|
worker->moveToThread(&remuxer);
|
|
remuxer.start();
|
|
|
|
//gcc-4.8 can't use QPointer<RemuxWorker> below
|
|
RemuxWorker *worker_ = worker;
|
|
connect(worker_, &RemuxWorker::updateProgress,
|
|
this, &OBSRemux::updateProgress);
|
|
connect(&remuxer, &QThread::finished, worker_, &QObject::deleteLater);
|
|
connect(worker_, &RemuxWorker::remuxFinished,
|
|
this, &OBSRemux::remuxFinished);
|
|
connect(this, &OBSRemux::remux, worker_, &RemuxWorker::remux);
|
|
|
|
// Guessing the GCC bug mentioned above would also affect
|
|
// QPointer<RemuxQueueModel>? Unsure.
|
|
RemuxQueueModel *queueModel_ = queueModel;
|
|
connect(queueModel_,
|
|
SIGNAL(rowsInserted(const QModelIndex &, int, int)),
|
|
this,
|
|
SLOT(rowCountChanged(const QModelIndex &, int, int)));
|
|
connect(queueModel_,
|
|
SIGNAL(rowsRemoved(const QModelIndex &, int, int)),
|
|
this,
|
|
SLOT(rowCountChanged(const QModelIndex &, int, int)));
|
|
|
|
QModelIndex index = queueModel->createIndex(0, 1);
|
|
QMetaObject::invokeMethod(ui->tableView,
|
|
"setCurrentIndex", Qt::QueuedConnection,
|
|
Q_ARG(const QModelIndex &, index));
|
|
}
|
|
|
|
bool OBSRemux::stopRemux()
|
|
{
|
|
if (!worker->isWorking)
|
|
return true;
|
|
|
|
// By locking the worker thread's mutex, we ensure that its
|
|
// update poll will be blocked as long as we're in here with
|
|
// the popup open.
|
|
QMutexLocker lock(&worker->updateMutex);
|
|
|
|
bool exit = false;
|
|
|
|
if (QMessageBox::critical(nullptr,
|
|
QTStr("Remux.ExitUnfinishedTitle"),
|
|
QTStr("Remux.ExitUnfinished"),
|
|
QMessageBox::Yes | QMessageBox::No,
|
|
QMessageBox::No) ==
|
|
QMessageBox::Yes) {
|
|
exit = true;
|
|
}
|
|
|
|
if (exit) {
|
|
// Inform the worker it should no longer be
|
|
// working. It will interrupt accordingly in
|
|
// its next update callback.
|
|
worker->isWorking = false;
|
|
}
|
|
|
|
return exit;
|
|
}
|
|
|
|
OBSRemux::~OBSRemux()
|
|
{
|
|
stopRemux();
|
|
remuxer.quit();
|
|
remuxer.wait();
|
|
}
|
|
|
|
void OBSRemux::rowCountChanged(const QModelIndex &, int, int)
|
|
{
|
|
// See if there are still any rows ready to remux. Change
|
|
// the state of the "go" button accordingly.
|
|
// There must be more than one row, since there will always be
|
|
// at least one row for the empty insertion point.
|
|
if (queueModel->rowCount() > 1) {
|
|
ui->buttonBox->button(QDialogButtonBox::Ok)->
|
|
setEnabled(true);
|
|
ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->
|
|
setEnabled(true);
|
|
ui->buttonBox->button(QDialogButtonBox::Reset)->
|
|
setEnabled(queueModel->canClearFinished());
|
|
} else {
|
|
ui->buttonBox->button(QDialogButtonBox::Ok)->
|
|
setEnabled(false);
|
|
ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->
|
|
setEnabled(false);
|
|
ui->buttonBox->button(QDialogButtonBox::Reset)->
|
|
setEnabled(false);
|
|
}
|
|
}
|
|
|
|
void OBSRemux::dropEvent(QDropEvent *ev)
|
|
{
|
|
QStringList urlList;
|
|
|
|
for (QUrl url : ev->mimeData()->urls()) {
|
|
QFileInfo fileInfo(url.toLocalFile());
|
|
|
|
if (fileInfo.isDir()) {
|
|
QStringList directoryFilter;
|
|
directoryFilter <<
|
|
"*.flv" <<
|
|
"*.mp4" <<
|
|
"*.mov" <<
|
|
"*.mkv" <<
|
|
"*.ts" <<
|
|
"*.m3u8";
|
|
|
|
QDirIterator dirIter(fileInfo.absoluteFilePath(),
|
|
directoryFilter, QDir::Files,
|
|
QDirIterator::Subdirectories);
|
|
|
|
while (dirIter.hasNext()) {
|
|
urlList.append(dirIter.next());
|
|
}
|
|
} else {
|
|
urlList.append(fileInfo.canonicalFilePath());
|
|
}
|
|
}
|
|
|
|
if (urlList.empty()) {
|
|
QMessageBox::information(nullptr,
|
|
QTStr("Remux.NoFilesAddedTitle"),
|
|
QTStr("Remux.NoFilesAdded"), QMessageBox::Ok);
|
|
} else {
|
|
QModelIndex insertIndex = queueModel->index(
|
|
queueModel->rowCount() - 1,
|
|
RemuxEntryColumn::InputPath);
|
|
queueModel->setData(insertIndex, urlList,
|
|
RemuxEntryRole::NewPathsToProcessRole);
|
|
}
|
|
}
|
|
|
|
void OBSRemux::dragEnterEvent(QDragEnterEvent *ev)
|
|
{
|
|
if (ev->mimeData()->hasUrls() && !worker->isWorking)
|
|
ev->accept();
|
|
}
|
|
|
|
void OBSRemux::beginRemux()
|
|
{
|
|
if (worker->isWorking) {
|
|
stopRemux();
|
|
return;
|
|
}
|
|
|
|
bool proceedWithRemux = true;
|
|
QFileInfoList overwriteFiles = queueModel->checkForOverwrites();
|
|
|
|
if (!overwriteFiles.empty()) {
|
|
QString message = QTStr("Remux.FileExists");
|
|
message += "\n\n";
|
|
|
|
for (QFileInfo fileInfo : overwriteFiles)
|
|
message += fileInfo.canonicalFilePath() + "\n";
|
|
|
|
if (OBSMessageBox::question(this,
|
|
QTStr("Remux.FileExistsTitle"), message)
|
|
!= QMessageBox::Yes)
|
|
proceedWithRemux = false;
|
|
}
|
|
|
|
if (!proceedWithRemux)
|
|
return;
|
|
|
|
// Set all jobs to "pending" first.
|
|
queueModel->beginProcessing();
|
|
|
|
ui->progressBar->setVisible(true);
|
|
ui->buttonBox->button(QDialogButtonBox::Ok)->
|
|
setText(QTStr("Remux.Stop"));
|
|
setAcceptDrops(false);
|
|
|
|
remuxNextEntry();
|
|
|
|
}
|
|
|
|
void OBSRemux::remuxNextEntry()
|
|
{
|
|
worker->lastProgress = 0.f;
|
|
|
|
QString inputPath, outputPath;
|
|
if (queueModel->beginNextEntry(inputPath, outputPath)) {
|
|
emit remux(inputPath, outputPath);
|
|
} else {
|
|
queueModel->endProcessing();
|
|
|
|
OBSMessageBox::information(this, QTStr("Remux.FinishedTitle"),
|
|
queueModel->checkForErrors()
|
|
? QTStr("Remux.FinishedError")
|
|
: QTStr("Remux.Finished"));
|
|
|
|
ui->progressBar->setVisible(false);
|
|
ui->buttonBox->button(QDialogButtonBox::Ok)->
|
|
setText(QTStr("Remux.Remux"));
|
|
ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)->
|
|
setEnabled(true);
|
|
ui->buttonBox->button(QDialogButtonBox::Reset)->
|
|
setEnabled(queueModel->canClearFinished());
|
|
setAcceptDrops(true);
|
|
}
|
|
}
|
|
|
|
void OBSRemux::closeEvent(QCloseEvent *event)
|
|
{
|
|
if (!stopRemux())
|
|
event->ignore();
|
|
else
|
|
QDialog::closeEvent(event);
|
|
}
|
|
|
|
void OBSRemux::reject()
|
|
{
|
|
if (!stopRemux())
|
|
return;
|
|
|
|
QDialog::reject();
|
|
}
|
|
|
|
void OBSRemux::updateProgress(float percent)
|
|
{
|
|
ui->progressBar->setValue(percent * 10);
|
|
}
|
|
|
|
void OBSRemux::remuxFinished(bool success)
|
|
{
|
|
queueModel->finishEntry(success);
|
|
remuxNextEntry();
|
|
}
|
|
|
|
void OBSRemux::clearFinished()
|
|
{
|
|
queueModel->clearFinished();
|
|
}
|
|
|
|
void OBSRemux::clearAll()
|
|
{
|
|
queueModel->clearAll();
|
|
}
|
|
|
|
/**********************************************************
|
|
Worker thread - Executes the libobs remux operation as a
|
|
background process.
|
|
**********************************************************/
|
|
|
|
void RemuxWorker::UpdateProgress(float percent)
|
|
{
|
|
if (abs(lastProgress - percent) < 0.1f)
|
|
return;
|
|
|
|
emit updateProgress(percent);
|
|
lastProgress = percent;
|
|
}
|
|
|
|
void RemuxWorker::remux(const QString &source, const QString &target)
|
|
{
|
|
isWorking = true;
|
|
|
|
auto callback = [](void *data, float percent)
|
|
{
|
|
RemuxWorker *rw = static_cast<RemuxWorker*>(data);
|
|
|
|
QMutexLocker lock(&rw->updateMutex);
|
|
|
|
rw->UpdateProgress(percent);
|
|
|
|
return rw->isWorking;
|
|
};
|
|
|
|
bool stopped = false;
|
|
bool success = false;
|
|
|
|
media_remux_job_t mr_job = nullptr;
|
|
if (media_remux_job_create(&mr_job,
|
|
QT_TO_UTF8(source),
|
|
QT_TO_UTF8(target))) {
|
|
|
|
success = media_remux_job_process(mr_job, callback,
|
|
this);
|
|
|
|
media_remux_job_destroy(mr_job);
|
|
|
|
stopped = !isWorking;
|
|
}
|
|
|
|
isWorking = false;
|
|
|
|
emit remuxFinished(!stopped && success);
|
|
}
|