window-remux.cpp 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990
  1. /******************************************************************************
  2. Copyright (C) 2014 by Ruwen Hahn <[email protected]>
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 2 of the License, or
  6. (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. ******************************************************************************/
  14. #include "window-remux.hpp"
  15. #include "obs-app.hpp"
  16. #include <QCloseEvent>
  17. #include <QDirIterator>
  18. #include <QItemDelegate>
  19. #include <QLineEdit>
  20. #include <QMessageBox>
  21. #include <QMimeData>
  22. #include <QPainter>
  23. #include <QPushButton>
  24. #include <QStandardItemModel>
  25. #include <QStyledItemDelegate>
  26. #include <QToolButton>
  27. #include <QTimer>
  28. #include "qt-wrappers.hpp"
  29. #include <memory>
  30. #include <cmath>
  31. using namespace std;
  32. enum RemuxEntryColumn {
  33. State,
  34. InputPath,
  35. OutputPath,
  36. Count
  37. };
  38. enum RemuxEntryRole { EntryStateRole = Qt::UserRole, NewPathsToProcessRole };
  39. /**********************************************************
  40. Delegate - Presents cells in the grid.
  41. **********************************************************/
  42. RemuxEntryPathItemDelegate::RemuxEntryPathItemDelegate(
  43. bool isOutput, const QString &defaultPath)
  44. : QStyledItemDelegate(), isOutput(isOutput), defaultPath(defaultPath)
  45. {
  46. }
  47. QWidget *RemuxEntryPathItemDelegate::createEditor(
  48. QWidget *parent, const QStyleOptionViewItem & /* option */,
  49. const QModelIndex &index) const
  50. {
  51. RemuxEntryState state =
  52. index.model()
  53. ->index(index.row(), RemuxEntryColumn::State)
  54. .data(RemuxEntryRole::EntryStateRole)
  55. .value<RemuxEntryState>();
  56. if (state == RemuxEntryState::Pending ||
  57. state == RemuxEntryState::InProgress) {
  58. // Never allow modification of rows that are
  59. // in progress.
  60. return Q_NULLPTR;
  61. } else if (isOutput && state != RemuxEntryState::Ready) {
  62. // Do not allow modification of output rows
  63. // that aren't associated with a valid input.
  64. return Q_NULLPTR;
  65. } else if (!isOutput && state == RemuxEntryState::Complete) {
  66. // Don't allow modification of rows that are
  67. // already complete.
  68. return Q_NULLPTR;
  69. } else {
  70. QSizePolicy buttonSizePolicy(
  71. QSizePolicy::Policy::Minimum,
  72. QSizePolicy::Policy::Expanding,
  73. QSizePolicy::ControlType::PushButton);
  74. QWidget *container = new QWidget(parent);
  75. auto browseCallback = [this, container]() {
  76. const_cast<RemuxEntryPathItemDelegate *>(this)
  77. ->handleBrowse(container);
  78. };
  79. auto clearCallback = [this, container]() {
  80. const_cast<RemuxEntryPathItemDelegate *>(this)
  81. ->handleClear(container);
  82. };
  83. QHBoxLayout *layout = new QHBoxLayout();
  84. layout->setContentsMargins(0, 0, 0, 0);
  85. layout->setSpacing(0);
  86. QLineEdit *text = new QLineEdit();
  87. text->setObjectName(QStringLiteral("text"));
  88. text->setSizePolicy(
  89. QSizePolicy(QSizePolicy::Policy::Expanding,
  90. QSizePolicy::Policy::Expanding,
  91. QSizePolicy::ControlType::LineEdit));
  92. layout->addWidget(text);
  93. QObject::connect(text, SIGNAL(editingFinished()), this,
  94. SLOT(updateText()));
  95. QToolButton *browseButton = new QToolButton();
  96. browseButton->setText("...");
  97. browseButton->setSizePolicy(buttonSizePolicy);
  98. layout->addWidget(browseButton);
  99. container->connect(browseButton, &QToolButton::clicked,
  100. browseCallback);
  101. // The "clear" button is not shown in output cells
  102. // or the insertion point's input cell.
  103. if (!isOutput && state != RemuxEntryState::Empty) {
  104. QToolButton *clearButton = new QToolButton();
  105. clearButton->setText("X");
  106. clearButton->setSizePolicy(buttonSizePolicy);
  107. layout->addWidget(clearButton);
  108. container->connect(clearButton, &QToolButton::clicked,
  109. clearCallback);
  110. }
  111. container->setLayout(layout);
  112. container->setFocusProxy(text);
  113. return container;
  114. }
  115. }
  116. void RemuxEntryPathItemDelegate::setEditorData(QWidget *editor,
  117. const QModelIndex &index) const
  118. {
  119. QLineEdit *text = editor->findChild<QLineEdit *>();
  120. text->setText(index.data().toString());
  121. editor->setProperty(PATH_LIST_PROP, QVariant());
  122. }
  123. void RemuxEntryPathItemDelegate::setModelData(QWidget *editor,
  124. QAbstractItemModel *model,
  125. const QModelIndex &index) const
  126. {
  127. // We use the PATH_LIST_PROP property to pass a list of
  128. // path strings from the editor widget into the model's
  129. // NewPathsToProcessRole. This is only used when paths
  130. // are selected through the "browse" or "delete" buttons
  131. // in the editor. If the user enters new text in the
  132. // text box, we simply pass that text on to the model
  133. // as normal text data in the default role.
  134. QVariant pathListProp = editor->property(PATH_LIST_PROP);
  135. if (pathListProp.isValid()) {
  136. QStringList list =
  137. editor->property(PATH_LIST_PROP).toStringList();
  138. if (isOutput) {
  139. if (list.size() > 0)
  140. model->setData(index, list);
  141. } else
  142. model->setData(index, list,
  143. RemuxEntryRole::NewPathsToProcessRole);
  144. } else {
  145. QLineEdit *lineEdit = editor->findChild<QLineEdit *>();
  146. model->setData(index, lineEdit->text());
  147. }
  148. }
  149. void RemuxEntryPathItemDelegate::paint(QPainter *painter,
  150. const QStyleOptionViewItem &option,
  151. const QModelIndex &index) const
  152. {
  153. RemuxEntryState state =
  154. index.model()
  155. ->index(index.row(), RemuxEntryColumn::State)
  156. .data(RemuxEntryRole::EntryStateRole)
  157. .value<RemuxEntryState>();
  158. QStyleOptionViewItem localOption = option;
  159. initStyleOption(&localOption, index);
  160. if (isOutput) {
  161. if (state != Ready) {
  162. QColor background = localOption.palette.color(
  163. QPalette::ColorGroup::Disabled,
  164. QPalette::ColorRole::Window);
  165. localOption.backgroundBrush = QBrush(background);
  166. }
  167. }
  168. QApplication::style()->drawControl(QStyle::CE_ItemViewItem,
  169. &localOption, painter);
  170. }
  171. void RemuxEntryPathItemDelegate::handleBrowse(QWidget *container)
  172. {
  173. QString OutputPattern = "(*.mp4 *.flv *.mov *.mkv *.ts *.m3u8)";
  174. QString InputPattern = "(*.flv *.mov *.mkv *.ts *.m3u8)";
  175. QLineEdit *text = container->findChild<QLineEdit *>();
  176. QString currentPath = text->text();
  177. if (currentPath.isEmpty())
  178. currentPath = defaultPath;
  179. bool isSet = false;
  180. if (isOutput) {
  181. QString newPath = SaveFile(container,
  182. QTStr("Remux.SelectTarget"),
  183. currentPath, OutputPattern);
  184. if (!newPath.isEmpty()) {
  185. container->setProperty(PATH_LIST_PROP,
  186. QStringList() << newPath);
  187. isSet = true;
  188. }
  189. } else {
  190. QStringList paths = OpenFiles(
  191. container, QTStr("Remux.SelectRecording"), currentPath,
  192. QTStr("Remux.OBSRecording") + QString(" ") +
  193. InputPattern);
  194. if (!paths.empty()) {
  195. container->setProperty(PATH_LIST_PROP, paths);
  196. isSet = true;
  197. }
  198. }
  199. if (isSet)
  200. emit commitData(container);
  201. }
  202. void RemuxEntryPathItemDelegate::handleClear(QWidget *container)
  203. {
  204. // An empty string list will indicate that the entry is being
  205. // blanked and should be deleted.
  206. container->setProperty(PATH_LIST_PROP, QStringList());
  207. emit commitData(container);
  208. }
  209. void RemuxEntryPathItemDelegate::updateText()
  210. {
  211. QLineEdit *lineEdit = dynamic_cast<QLineEdit *>(sender());
  212. QWidget *editor = lineEdit->parentWidget();
  213. emit commitData(editor);
  214. }
  215. /**********************************************************
  216. Model - Manages the queue's data
  217. **********************************************************/
  218. int RemuxQueueModel::rowCount(const QModelIndex &) const
  219. {
  220. return queue.length() + (isProcessing ? 0 : 1);
  221. }
  222. int RemuxQueueModel::columnCount(const QModelIndex &) const
  223. {
  224. return RemuxEntryColumn::Count;
  225. }
  226. QVariant RemuxQueueModel::data(const QModelIndex &index, int role) const
  227. {
  228. QVariant result = QVariant();
  229. if (index.row() >= queue.length()) {
  230. return QVariant();
  231. } else if (role == Qt::DisplayRole) {
  232. switch (index.column()) {
  233. case RemuxEntryColumn::InputPath:
  234. result = queue[index.row()].sourcePath;
  235. break;
  236. case RemuxEntryColumn::OutputPath:
  237. result = queue[index.row()].targetPath;
  238. break;
  239. }
  240. } else if (role == Qt::DecorationRole &&
  241. index.column() == RemuxEntryColumn::State) {
  242. result = getIcon(queue[index.row()].state);
  243. } else if (role == RemuxEntryRole::EntryStateRole) {
  244. result = queue[index.row()].state;
  245. }
  246. return result;
  247. }
  248. QVariant RemuxQueueModel::headerData(int section, Qt::Orientation orientation,
  249. int role) const
  250. {
  251. QVariant result = QVariant();
  252. if (role == Qt::DisplayRole &&
  253. orientation == Qt::Orientation::Horizontal) {
  254. switch (section) {
  255. case RemuxEntryColumn::State:
  256. result = QString();
  257. break;
  258. case RemuxEntryColumn::InputPath:
  259. result = QTStr("Remux.SourceFile");
  260. break;
  261. case RemuxEntryColumn::OutputPath:
  262. result = QTStr("Remux.TargetFile");
  263. break;
  264. }
  265. }
  266. return result;
  267. }
  268. Qt::ItemFlags RemuxQueueModel::flags(const QModelIndex &index) const
  269. {
  270. Qt::ItemFlags flags = QAbstractTableModel::flags(index);
  271. if (index.column() == RemuxEntryColumn::InputPath) {
  272. flags |= Qt::ItemIsEditable;
  273. } else if (index.column() == RemuxEntryColumn::OutputPath &&
  274. index.row() != queue.length()) {
  275. flags |= Qt::ItemIsEditable;
  276. }
  277. return flags;
  278. }
  279. bool RemuxQueueModel::setData(const QModelIndex &index, const QVariant &value,
  280. int role)
  281. {
  282. bool success = false;
  283. if (role == RemuxEntryRole::NewPathsToProcessRole) {
  284. QStringList pathList = value.toStringList();
  285. if (pathList.size() == 0) {
  286. if (index.row() < queue.size()) {
  287. beginRemoveRows(QModelIndex(), index.row(),
  288. index.row());
  289. queue.removeAt(index.row());
  290. endRemoveRows();
  291. }
  292. } else {
  293. if (pathList.size() > 1 &&
  294. index.row() < queue.length()) {
  295. queue[index.row()].sourcePath = pathList[0];
  296. checkInputPath(index.row());
  297. pathList.removeAt(0);
  298. success = true;
  299. }
  300. if (pathList.size() > 0) {
  301. int row = index.row();
  302. int lastRow = row + pathList.size() - 1;
  303. beginInsertRows(QModelIndex(), row, lastRow);
  304. for (QString path : pathList) {
  305. RemuxQueueEntry entry;
  306. entry.sourcePath = path;
  307. queue.insert(row, entry);
  308. row++;
  309. }
  310. endInsertRows();
  311. for (row = index.row(); row <= lastRow; row++) {
  312. checkInputPath(row);
  313. }
  314. success = true;
  315. }
  316. }
  317. } else if (index.row() == queue.length()) {
  318. QString path = value.toString();
  319. if (!path.isEmpty()) {
  320. RemuxQueueEntry entry;
  321. entry.sourcePath = path;
  322. beginInsertRows(QModelIndex(), queue.length() + 1,
  323. queue.length() + 1);
  324. queue.append(entry);
  325. endInsertRows();
  326. checkInputPath(index.row());
  327. success = true;
  328. }
  329. } else {
  330. QString path = value.toString();
  331. if (path.isEmpty()) {
  332. if (index.column() == RemuxEntryColumn::InputPath) {
  333. beginRemoveRows(QModelIndex(), index.row(),
  334. index.row());
  335. queue.removeAt(index.row());
  336. endRemoveRows();
  337. }
  338. } else {
  339. switch (index.column()) {
  340. case RemuxEntryColumn::InputPath:
  341. queue[index.row()].sourcePath =
  342. value.toString();
  343. checkInputPath(index.row());
  344. success = true;
  345. break;
  346. case RemuxEntryColumn::OutputPath:
  347. queue[index.row()].targetPath =
  348. value.toString();
  349. emit dataChanged(index, index);
  350. success = true;
  351. break;
  352. }
  353. }
  354. }
  355. return success;
  356. }
  357. QVariant RemuxQueueModel::getIcon(RemuxEntryState state)
  358. {
  359. QVariant icon;
  360. QStyle *style = QApplication::style();
  361. switch (state) {
  362. case RemuxEntryState::Complete:
  363. icon = style->standardIcon(QStyle::SP_DialogApplyButton);
  364. break;
  365. case RemuxEntryState::InProgress:
  366. icon = style->standardIcon(QStyle::SP_ArrowRight);
  367. break;
  368. case RemuxEntryState::Error:
  369. icon = style->standardIcon(QStyle::SP_DialogCancelButton);
  370. break;
  371. case RemuxEntryState::InvalidPath:
  372. icon = style->standardIcon(QStyle::SP_MessageBoxWarning);
  373. break;
  374. default:
  375. break;
  376. }
  377. return icon;
  378. }
  379. void RemuxQueueModel::checkInputPath(int row)
  380. {
  381. RemuxQueueEntry &entry = queue[row];
  382. if (entry.sourcePath.isEmpty()) {
  383. entry.state = RemuxEntryState::Empty;
  384. } else {
  385. entry.sourcePath = QDir::toNativeSeparators(entry.sourcePath);
  386. QFileInfo fileInfo(entry.sourcePath);
  387. if (fileInfo.exists())
  388. entry.state = RemuxEntryState::Ready;
  389. else
  390. entry.state = RemuxEntryState::InvalidPath;
  391. if (entry.state == RemuxEntryState::Ready)
  392. entry.targetPath = QDir::toNativeSeparators(
  393. fileInfo.path() + QDir::separator() +
  394. fileInfo.completeBaseName() + ".mp4");
  395. }
  396. if (entry.state == RemuxEntryState::Ready && isProcessing)
  397. entry.state = RemuxEntryState::Pending;
  398. emit dataChanged(index(row, 0), index(row, RemuxEntryColumn::Count));
  399. }
  400. QFileInfoList RemuxQueueModel::checkForOverwrites() const
  401. {
  402. QFileInfoList list;
  403. for (const RemuxQueueEntry &entry : queue) {
  404. if (entry.state == RemuxEntryState::Ready) {
  405. QFileInfo fileInfo(entry.targetPath);
  406. if (fileInfo.exists()) {
  407. list.append(fileInfo);
  408. }
  409. }
  410. }
  411. return list;
  412. }
  413. bool RemuxQueueModel::checkForErrors() const
  414. {
  415. bool hasErrors = false;
  416. for (const RemuxQueueEntry &entry : queue) {
  417. if (entry.state == RemuxEntryState::Error) {
  418. hasErrors = true;
  419. break;
  420. }
  421. }
  422. return hasErrors;
  423. }
  424. void RemuxQueueModel::clearAll()
  425. {
  426. beginRemoveRows(QModelIndex(), 0, queue.size() - 1);
  427. queue.clear();
  428. endRemoveRows();
  429. }
  430. void RemuxQueueModel::clearFinished()
  431. {
  432. int index = 0;
  433. for (index = 0; index < queue.size(); index++) {
  434. const RemuxQueueEntry &entry = queue[index];
  435. if (entry.state == RemuxEntryState::Complete) {
  436. beginRemoveRows(QModelIndex(), index, index);
  437. queue.removeAt(index);
  438. endRemoveRows();
  439. index--;
  440. }
  441. }
  442. }
  443. bool RemuxQueueModel::canClearFinished() const
  444. {
  445. bool canClearFinished = false;
  446. for (const RemuxQueueEntry &entry : queue)
  447. if (entry.state == RemuxEntryState::Complete) {
  448. canClearFinished = true;
  449. break;
  450. }
  451. return canClearFinished;
  452. }
  453. void RemuxQueueModel::beginProcessing()
  454. {
  455. for (RemuxQueueEntry &entry : queue)
  456. if (entry.state == RemuxEntryState::Ready)
  457. entry.state = RemuxEntryState::Pending;
  458. // Signal that the insertion point no longer exists.
  459. beginRemoveRows(QModelIndex(), queue.length(), queue.length());
  460. endRemoveRows();
  461. isProcessing = true;
  462. emit dataChanged(index(0, RemuxEntryColumn::State),
  463. index(queue.length(), RemuxEntryColumn::State));
  464. }
  465. void RemuxQueueModel::endProcessing()
  466. {
  467. for (RemuxQueueEntry &entry : queue) {
  468. if (entry.state == RemuxEntryState::Pending) {
  469. entry.state = RemuxEntryState::Ready;
  470. }
  471. }
  472. // Signal that the insertion point exists again.
  473. if (!autoRemux) {
  474. beginInsertRows(QModelIndex(), queue.length(), queue.length());
  475. endInsertRows();
  476. }
  477. isProcessing = false;
  478. emit dataChanged(index(0, RemuxEntryColumn::State),
  479. index(queue.length(), RemuxEntryColumn::State));
  480. }
  481. bool RemuxQueueModel::beginNextEntry(QString &inputPath, QString &outputPath)
  482. {
  483. bool anyStarted = false;
  484. for (int row = 0; row < queue.length(); row++) {
  485. RemuxQueueEntry &entry = queue[row];
  486. if (entry.state == RemuxEntryState::Pending) {
  487. entry.state = RemuxEntryState::InProgress;
  488. inputPath = entry.sourcePath;
  489. outputPath = entry.targetPath;
  490. QModelIndex index =
  491. this->index(row, RemuxEntryColumn::State);
  492. emit dataChanged(index, index);
  493. anyStarted = true;
  494. break;
  495. }
  496. }
  497. return anyStarted;
  498. }
  499. void RemuxQueueModel::finishEntry(bool success)
  500. {
  501. for (int row = 0; row < queue.length(); row++) {
  502. RemuxQueueEntry &entry = queue[row];
  503. if (entry.state == RemuxEntryState::InProgress) {
  504. if (success)
  505. entry.state = RemuxEntryState::Complete;
  506. else
  507. entry.state = RemuxEntryState::Error;
  508. QModelIndex index =
  509. this->index(row, RemuxEntryColumn::State);
  510. emit dataChanged(index, index);
  511. break;
  512. }
  513. }
  514. }
  515. /**********************************************************
  516. The actual remux window implementation
  517. **********************************************************/
  518. OBSRemux::OBSRemux(const char *path, QWidget *parent, bool autoRemux_)
  519. : QDialog(parent),
  520. queueModel(new RemuxQueueModel),
  521. worker(new RemuxWorker()),
  522. ui(new Ui::OBSRemux),
  523. recPath(path),
  524. autoRemux(autoRemux_)
  525. {
  526. setAcceptDrops(true);
  527. setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
  528. ui->setupUi(this);
  529. ui->progressBar->setVisible(false);
  530. ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
  531. ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)
  532. ->setEnabled(false);
  533. if (autoRemux) {
  534. resize(280, 40);
  535. ui->tableView->hide();
  536. ui->buttonBox->hide();
  537. ui->label->hide();
  538. }
  539. ui->progressBar->setMinimum(0);
  540. ui->progressBar->setMaximum(1000);
  541. ui->progressBar->setValue(0);
  542. ui->tableView->setModel(queueModel);
  543. ui->tableView->setItemDelegateForColumn(
  544. RemuxEntryColumn::InputPath,
  545. new RemuxEntryPathItemDelegate(false, recPath));
  546. ui->tableView->setItemDelegateForColumn(
  547. RemuxEntryColumn::OutputPath,
  548. new RemuxEntryPathItemDelegate(true, recPath));
  549. ui->tableView->horizontalHeader()->setSectionResizeMode(
  550. QHeaderView::ResizeMode::Stretch);
  551. ui->tableView->horizontalHeader()->setSectionResizeMode(
  552. RemuxEntryColumn::State, QHeaderView::ResizeMode::Fixed);
  553. ui->tableView->setEditTriggers(
  554. QAbstractItemView::EditTrigger::CurrentChanged);
  555. installEventFilter(CreateShortcutFilter());
  556. ui->buttonBox->button(QDialogButtonBox::Ok)
  557. ->setText(QTStr("Remux.Remux"));
  558. ui->buttonBox->button(QDialogButtonBox::Reset)
  559. ->setText(QTStr("Remux.ClearFinished"));
  560. ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)
  561. ->setText(QTStr("Remux.ClearAll"));
  562. ui->buttonBox->button(QDialogButtonBox::Reset)->setDisabled(true);
  563. connect(ui->buttonBox->button(QDialogButtonBox::Ok), SIGNAL(clicked()),
  564. this, SLOT(beginRemux()));
  565. connect(ui->buttonBox->button(QDialogButtonBox::Reset),
  566. SIGNAL(clicked()), this, SLOT(clearFinished()));
  567. connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults),
  568. SIGNAL(clicked()), this, SLOT(clearAll()));
  569. connect(ui->buttonBox->button(QDialogButtonBox::Close),
  570. SIGNAL(clicked()), this, SLOT(close()));
  571. worker->moveToThread(&remuxer);
  572. remuxer.start();
  573. //gcc-4.8 can't use QPointer<RemuxWorker> below
  574. RemuxWorker *worker_ = worker;
  575. connect(worker_, &RemuxWorker::updateProgress, this,
  576. &OBSRemux::updateProgress);
  577. connect(&remuxer, &QThread::finished, worker_, &QObject::deleteLater);
  578. connect(worker_, &RemuxWorker::remuxFinished, this,
  579. &OBSRemux::remuxFinished);
  580. connect(this, &OBSRemux::remux, worker_, &RemuxWorker::remux);
  581. // Guessing the GCC bug mentioned above would also affect
  582. // QPointer<RemuxQueueModel>? Unsure.
  583. RemuxQueueModel *queueModel_ = queueModel;
  584. connect(queueModel_,
  585. SIGNAL(rowsInserted(const QModelIndex &, int, int)), this,
  586. SLOT(rowCountChanged(const QModelIndex &, int, int)));
  587. connect(queueModel_, SIGNAL(rowsRemoved(const QModelIndex &, int, int)),
  588. this, SLOT(rowCountChanged(const QModelIndex &, int, int)));
  589. QModelIndex index = queueModel->createIndex(0, 1);
  590. QMetaObject::invokeMethod(ui->tableView, "setCurrentIndex",
  591. Qt::QueuedConnection,
  592. Q_ARG(const QModelIndex &, index));
  593. }
  594. bool OBSRemux::stopRemux()
  595. {
  596. if (!worker->isWorking)
  597. return true;
  598. // By locking the worker thread's mutex, we ensure that its
  599. // update poll will be blocked as long as we're in here with
  600. // the popup open.
  601. QMutexLocker lock(&worker->updateMutex);
  602. bool exit = false;
  603. if (QMessageBox::critical(nullptr, QTStr("Remux.ExitUnfinishedTitle"),
  604. QTStr("Remux.ExitUnfinished"),
  605. QMessageBox::Yes | QMessageBox::No,
  606. QMessageBox::No) == QMessageBox::Yes) {
  607. exit = true;
  608. }
  609. if (exit) {
  610. // Inform the worker it should no longer be
  611. // working. It will interrupt accordingly in
  612. // its next update callback.
  613. worker->isWorking = false;
  614. }
  615. return exit;
  616. }
  617. OBSRemux::~OBSRemux()
  618. {
  619. stopRemux();
  620. remuxer.quit();
  621. remuxer.wait();
  622. }
  623. void OBSRemux::rowCountChanged(const QModelIndex &, int, int)
  624. {
  625. // See if there are still any rows ready to remux. Change
  626. // the state of the "go" button accordingly.
  627. // There must be more than one row, since there will always be
  628. // at least one row for the empty insertion point.
  629. if (queueModel->rowCount() > 1) {
  630. ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
  631. ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)
  632. ->setEnabled(true);
  633. ui->buttonBox->button(QDialogButtonBox::Reset)
  634. ->setEnabled(queueModel->canClearFinished());
  635. } else {
  636. ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
  637. ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)
  638. ->setEnabled(false);
  639. ui->buttonBox->button(QDialogButtonBox::Reset)
  640. ->setEnabled(false);
  641. }
  642. }
  643. void OBSRemux::dropEvent(QDropEvent *ev)
  644. {
  645. QStringList urlList;
  646. for (QUrl url : ev->mimeData()->urls()) {
  647. QFileInfo fileInfo(url.toLocalFile());
  648. if (fileInfo.isDir()) {
  649. QStringList directoryFilter;
  650. directoryFilter << "*.flv"
  651. << "*.mp4"
  652. << "*.mov"
  653. << "*.mkv"
  654. << "*.ts"
  655. << "*.m3u8";
  656. QDirIterator dirIter(fileInfo.absoluteFilePath(),
  657. directoryFilter, QDir::Files,
  658. QDirIterator::Subdirectories);
  659. while (dirIter.hasNext()) {
  660. urlList.append(dirIter.next());
  661. }
  662. } else {
  663. urlList.append(fileInfo.canonicalFilePath());
  664. }
  665. }
  666. if (urlList.empty()) {
  667. QMessageBox::information(nullptr,
  668. QTStr("Remux.NoFilesAddedTitle"),
  669. QTStr("Remux.NoFilesAdded"),
  670. QMessageBox::Ok);
  671. } else if (!autoRemux) {
  672. QModelIndex insertIndex =
  673. queueModel->index(queueModel->rowCount() - 1,
  674. RemuxEntryColumn::InputPath);
  675. queueModel->setData(insertIndex, urlList,
  676. RemuxEntryRole::NewPathsToProcessRole);
  677. }
  678. }
  679. void OBSRemux::dragEnterEvent(QDragEnterEvent *ev)
  680. {
  681. if (ev->mimeData()->hasUrls() && !worker->isWorking)
  682. ev->accept();
  683. }
  684. void OBSRemux::beginRemux()
  685. {
  686. if (worker->isWorking) {
  687. stopRemux();
  688. return;
  689. }
  690. bool proceedWithRemux = true;
  691. QFileInfoList overwriteFiles = queueModel->checkForOverwrites();
  692. if (!overwriteFiles.empty()) {
  693. QString message = QTStr("Remux.FileExists");
  694. message += "\n\n";
  695. for (QFileInfo fileInfo : overwriteFiles)
  696. message += fileInfo.canonicalFilePath() + "\n";
  697. if (OBSMessageBox::question(this,
  698. QTStr("Remux.FileExistsTitle"),
  699. message) != QMessageBox::Yes)
  700. proceedWithRemux = false;
  701. }
  702. if (!proceedWithRemux)
  703. return;
  704. // Set all jobs to "pending" first.
  705. queueModel->beginProcessing();
  706. ui->progressBar->setVisible(true);
  707. ui->buttonBox->button(QDialogButtonBox::Ok)
  708. ->setText(QTStr("Remux.Stop"));
  709. setAcceptDrops(false);
  710. remuxNextEntry();
  711. }
  712. void OBSRemux::AutoRemux(QString inFile, QString outFile)
  713. {
  714. if (inFile != "" && outFile != "" && autoRemux) {
  715. emit remux(inFile, outFile);
  716. autoRemuxFile = inFile;
  717. }
  718. }
  719. void OBSRemux::remuxNextEntry()
  720. {
  721. worker->lastProgress = 0.f;
  722. QString inputPath, outputPath;
  723. if (queueModel->beginNextEntry(inputPath, outputPath)) {
  724. emit remux(inputPath, outputPath);
  725. } else {
  726. queueModel->autoRemux = autoRemux;
  727. queueModel->endProcessing();
  728. if (!autoRemux) {
  729. OBSMessageBox::information(
  730. this, QTStr("Remux.FinishedTitle"),
  731. queueModel->checkForErrors()
  732. ? QTStr("Remux.FinishedError")
  733. : QTStr("Remux.Finished"));
  734. }
  735. ui->progressBar->setVisible(autoRemux);
  736. ui->buttonBox->button(QDialogButtonBox::Ok)
  737. ->setText(QTStr("Remux.Remux"));
  738. ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)
  739. ->setEnabled(true);
  740. ui->buttonBox->button(QDialogButtonBox::Reset)
  741. ->setEnabled(queueModel->canClearFinished());
  742. setAcceptDrops(true);
  743. }
  744. }
  745. void OBSRemux::closeEvent(QCloseEvent *event)
  746. {
  747. if (!stopRemux())
  748. event->ignore();
  749. else
  750. QDialog::closeEvent(event);
  751. }
  752. void OBSRemux::reject()
  753. {
  754. if (!stopRemux())
  755. return;
  756. QDialog::reject();
  757. }
  758. void OBSRemux::updateProgress(float percent)
  759. {
  760. ui->progressBar->setValue(percent * 10);
  761. }
  762. void OBSRemux::remuxFinished(bool success)
  763. {
  764. ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
  765. queueModel->finishEntry(success);
  766. if (autoRemux && autoRemuxFile != "") {
  767. QTimer::singleShot(3000, this, SLOT(close()));
  768. }
  769. remuxNextEntry();
  770. }
  771. void OBSRemux::clearFinished()
  772. {
  773. queueModel->clearFinished();
  774. }
  775. void OBSRemux::clearAll()
  776. {
  777. queueModel->clearAll();
  778. }
  779. /**********************************************************
  780. Worker thread - Executes the libobs remux operation as a
  781. background process.
  782. **********************************************************/
  783. void RemuxWorker::UpdateProgress(float percent)
  784. {
  785. if (abs(lastProgress - percent) < 0.1f)
  786. return;
  787. emit updateProgress(percent);
  788. lastProgress = percent;
  789. }
  790. void RemuxWorker::remux(const QString &source, const QString &target)
  791. {
  792. isWorking = true;
  793. auto callback = [](void *data, float percent) {
  794. RemuxWorker *rw = static_cast<RemuxWorker *>(data);
  795. QMutexLocker lock(&rw->updateMutex);
  796. rw->UpdateProgress(percent);
  797. return rw->isWorking;
  798. };
  799. bool stopped = false;
  800. bool success = false;
  801. media_remux_job_t mr_job = nullptr;
  802. if (media_remux_job_create(&mr_job, QT_TO_UTF8(source),
  803. QT_TO_UTF8(target))) {
  804. success = media_remux_job_process(mr_job, callback, this);
  805. media_remux_job_destroy(mr_job);
  806. stopped = !isWorking;
  807. }
  808. isWorking = false;
  809. emit remuxFinished(!stopped && success);
  810. }