window-remux.cpp 25 KB

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