Pārlūkot izejas kodu

Add UI for remuxing recordings via FFmpeg

Palana 11 gadi atpakaļ
vecāks
revīzija
c9ee436e1c

+ 4 - 1
obs/CMakeLists.txt

@@ -97,6 +97,7 @@ set(obs_SOURCES
 	window-basic-preview.cpp
 	window-namedialog.cpp
 	window-log-reply.cpp
+	window-remux.cpp
 	properties-view.cpp
 	volume-control.cpp
 	qt-wrappers.cpp)
@@ -116,6 +117,7 @@ set(obs_HEADERS
 	window-basic-preview.hpp
 	window-namedialog.hpp
 	window-log-reply.hpp
+	window-remux.hpp
 	properties-view.hpp
 	display-helpers.hpp
 	volume-control.hpp
@@ -131,7 +133,8 @@ set(obs_UI
 	forms/OBSBasicSettings.ui
 	forms/OBSBasicSourceSelect.ui
 	forms/OBSBasicInteraction.ui
-	forms/OBSBasicProperties.ui)
+	forms/OBSBasicProperties.ui
+	forms/OBSRemux.ui)
 
 set(obs_QRC
 	forms/obs.qrc)

+ 16 - 0
obs/data/locale/en-US.ini

@@ -61,6 +61,21 @@ LicenseAgreement.ClickIAgreeToContinue="If you accept the terms of the agreement
 LicenseAgreement.IAgree="I Agree"
 LicenseAgreement.Exit="Exit"
 
+# remux dialog
+Remux.SourceFile="OBS Recording"
+Remux.TargetFile="Target File"
+Remux.Remux="Remux"
+Remux.RecordingPattern="OBS Recording (*.flv)"
+Remux.FinishedTitle="Remuxing finished"
+Remux.Finished="Recording remuxed"
+Remux.FinishedError="Recording remuxed, but the file may be incomplete"
+Remux.SelectRecording="Select OBS Recording …"
+Remux.SelectTarget="Select target file …"
+Remux.FileExistsTitle="Target file exists"
+Remux.FileExists="Target file exists, do you want to replace it?"
+Remux.ExitUnfinishedTitle="Remuxing in progress"
+Remux.ExitUnfinished="Remuxing is not finished, stopping now may render the target file unusable.\nAre you sure you want to stop remuxing?"
+
 # update dialog
 UpdateAvailable="New Update Available"
 UpdateAvailable.Text="Version %1.%2.%3 is now available.  <a href='%4'>Click here to download</a>"
@@ -148,6 +163,7 @@ Basic.MainMenu.File="&File"
 Basic.MainMenu.File.Export="&Export"
 Basic.MainMenu.File.Import="&Import"
 Basic.MainMenu.File.ShowRecordings="Show &Recordings"
+Basic.MainMenu.File.Remux="&Remux Recordings"
 Basic.MainMenu.File.Settings="&Settings"
 Basic.MainMenu.File.Exit="E&xit"
 

+ 6 - 0
obs/forms/OBSBasic.ui

@@ -457,6 +457,7 @@
     <addaction name="action_Open"/>
     <addaction name="separator"/>
     <addaction name="actionShow_Recordings"/>
+    <addaction name="actionRemux"/>
     <addaction name="separator"/>
     <addaction name="action_Settings"/>
     <addaction name="separator"/>
@@ -651,6 +652,11 @@
     <string>Basic.MainMenu.File.ShowRecordings</string>
    </property>
   </action>
+  <action name="actionRemux">
+   <property name="text">
+    <string>Basic.MainMenu.File.Remux</string>
+   </property>
+  </action>
   <action name="action_Settings">
    <property name="text">
     <string>Basic.MainMenu.File.Settings</string>

+ 119 - 0
obs/forms/OBSRemux.ui

@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OBSRemux</class>
+ <widget class="QDialog" name="OBSRemux">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>491</width>
+    <height>124</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Dialog</string>
+  </property>
+  <widget class="QProgressBar" name="progressBar">
+   <property name="geometry">
+    <rect>
+     <x>10</x>
+     <y>90</y>
+     <width>351</width>
+     <height>23</height>
+    </rect>
+   </property>
+   <property name="value">
+    <number>24</number>
+   </property>
+  </widget>
+  <widget class="QWidget" name="formLayoutWidget">
+   <property name="geometry">
+    <rect>
+     <x>10</x>
+     <y>10</y>
+     <width>471</width>
+     <height>71</height>
+    </rect>
+   </property>
+   <layout class="QFormLayout" name="formLayout">
+    <property name="fieldGrowthPolicy">
+     <enum>QFormLayout::AllNonFixedFieldsGrow</enum>
+    </property>
+    <property name="verticalSpacing">
+     <number>6</number>
+    </property>
+    <item row="1" column="0">
+     <widget class="QLabel" name="label">
+      <property name="text">
+       <string>Remux.SourceFile</string>
+      </property>
+     </widget>
+    </item>
+    <item row="2" column="0">
+     <widget class="QLabel" name="label_2">
+      <property name="text">
+       <string>Remux.TargetFile</string>
+      </property>
+     </widget>
+    </item>
+    <item row="1" column="1">
+     <layout class="QHBoxLayout" name="horizontalLayout_2">
+      <item>
+       <widget class="QLineEdit" name="sourceFile">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="browseSource">
+        <property name="text">
+         <string>Browse</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </item>
+    <item row="2" column="1">
+     <layout class="QHBoxLayout" name="horizontalLayout_3">
+      <item>
+       <widget class="QLineEdit" name="targetFile">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="browseTarget">
+        <property name="text">
+         <string>Browse</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QPushButton" name="remux">
+   <property name="geometry">
+    <rect>
+     <x>370</x>
+     <y>90</y>
+     <width>111</width>
+     <height>23</height>
+    </rect>
+   </property>
+   <property name="text">
+    <string>Remux.Remux</string>
+   </property>
+  </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 9 - 0
obs/window-basic-main.cpp

@@ -38,6 +38,7 @@
 #include "window-basic-main.hpp"
 #include "window-basic-properties.hpp"
 #include "window-log-reply.hpp"
+#include "window-remux.hpp"
 #include "qt-wrappers.hpp"
 #include "display-helpers.hpp"
 #include "volume-control.hpp"
@@ -1474,6 +1475,14 @@ void OBSBasic::on_actionShow_Recordings_triggered()
 	QDesktopServices::openUrl(QUrl::fromLocalFile(path));
 }
 
+void OBSBasic::on_actionRemux_triggered()
+{
+	const char *path = config_get_string(basicConfig,
+			"SimpleOutput", "FilePath");
+	OBSRemux remux(path, this);
+	remux.exec();
+}
+
 void OBSBasic::on_action_Settings_triggered()
 {
 	OBSBasicSettings settings(this);

+ 1 - 0
obs/window-basic-main.hpp

@@ -233,6 +233,7 @@ private slots:
 	void on_action_Open_triggered();
 	void on_action_Save_triggered();
 	void on_actionShow_Recordings_triggered();
+	void on_actionRemux_triggered();
 	void on_action_Settings_triggered();
 	void on_actionShowLogs_triggered();
 	void on_actionUploadCurrentLog_triggered();

+ 228 - 0
obs/window-remux.cpp

@@ -0,0 +1,228 @@
+/******************************************************************************
+    Copyright (C) 2014 by Ruwen Hahn <[email protected]>
+
+    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 <QFileDialog>
+#include <QFileInfo>
+#include <QMessageBox>
+
+#include "qt-wrappers.hpp"
+
+#include <memory>
+#include <cmath>
+
+using namespace std;
+
+OBSRemux::OBSRemux(const char *path, QWidget *parent)
+	: QDialog (parent),
+	  worker  (new RemuxWorker),
+	  ui      (new Ui::OBSRemux),
+	  recPath (path)
+{
+	ui->setupUi(this);
+
+	ui->progressBar->setVisible(false);
+	ui->remux->setEnabled(false);
+	ui->targetFile->setEnabled(false);
+	ui->browseTarget->setEnabled(false);
+
+	ui->progressBar->setMinimum(0);
+	ui->progressBar->setMaximum(1000);
+	ui->progressBar->setValue(0);
+	
+	connect(ui->browseSource, &QPushButton::clicked,
+			[&]() { BrowseInput(); });
+	connect(ui->browseTarget, &QPushButton::clicked,
+			[&]() { BrowseOutput(); });
+	connect(ui->remux, &QPushButton::clicked, [&]() { Remux(); });
+
+	connect(ui->sourceFile, &QLineEdit::textChanged,
+			this, &OBSRemux::inputChanged);
+
+	worker->moveToThread(&remuxer);
+
+	//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);
+}
+
+bool OBSRemux::Stop()
+{
+	if (!worker->job)
+		return true;
+
+	if (QMessageBox::critical(nullptr,
+				QTStr("Remux.ExitUnfinishedTitle"),
+				QTStr("Remux.ExitUnfinished"),
+				QMessageBox::Yes | QMessageBox::No,
+				QMessageBox::No) ==
+			QMessageBox::Yes) {
+		os_event_signal(worker->stop);
+		return true;
+	}
+
+	return false;
+}
+
+OBSRemux::~OBSRemux()
+{
+	Stop();
+	remuxer.quit();
+	remuxer.wait();
+}
+
+void OBSRemux::BrowseInput()
+{
+	QString path = ui->sourceFile->text();
+	if (path.isEmpty())
+		path = recPath;
+
+	path = QFileDialog::getOpenFileName(this,
+			QTStr("Remux.SelectRecording"), path,
+			QTStr("Remux.RecordingPattern"));
+
+	inputChanged(path);
+}
+
+void OBSRemux::inputChanged(const QString &path)
+{
+	if (!QFileInfo::exists(path)) {
+		ui->remux->setEnabled(false);
+		return;
+	}
+
+	ui->sourceFile->setText(path);
+	ui->remux->setEnabled(true);
+
+	QFileInfo fi(path);
+	QString mp4 = fi.path() + "/" + fi.baseName() + ".mp4";
+	ui->targetFile->setText(mp4);
+
+	ui->targetFile->setEnabled(true);
+	ui->browseTarget->setEnabled(true);
+}
+
+void OBSRemux::BrowseOutput()
+{
+	QString path(ui->targetFile->text());
+	path = QFileDialog::getSaveFileName(this, QTStr("Remux.SelectTarget"),
+				path, "(*.mp4)");
+
+	if (path.isEmpty())
+		return;
+
+	ui->targetFile->setText(path);
+}
+
+void OBSRemux::Remux()
+{
+	if (QFileInfo::exists(ui->targetFile->text()))
+		if (QMessageBox::question(this, QTStr("Remux.FileExistsTitle"),
+					QTStr("Remux.FileExists"),
+					QMessageBox::Yes | QMessageBox::No) !=
+				QMessageBox::Yes)
+			return;
+
+	media_remux_job_t mr_job = nullptr;
+	if (!media_remux_job_create(&mr_job, QT_TO_UTF8(ui->sourceFile->text()),
+				QT_TO_UTF8(ui->targetFile->text())))
+		return;
+
+	worker->job = job_t(mr_job, media_remux_job_destroy);
+	worker->lastProgress = 0.f;
+
+	ui->progressBar->setVisible(true);
+	ui->remux->setEnabled(false);
+
+	if (!remuxer.isRunning())
+		remuxer.start();
+	emit remux();
+}
+
+void OBSRemux::closeEvent(QCloseEvent *event)
+{
+	if (!Stop())
+		event->ignore();
+	else
+		QDialog::closeEvent(event);
+}
+
+void OBSRemux::reject()
+{
+	if (!Stop())
+		return;
+
+	QDialog::reject();
+}
+
+void OBSRemux::updateProgress(float percent)
+{
+	ui->progressBar->setValue(percent * 10);
+}
+
+void OBSRemux::remuxFinished(bool success)
+{
+	QMessageBox::information(this, QTStr("Remux.FinishedTitle"),
+			success ?
+			QTStr("Remux.Finished") : QTStr("Remux.FinishedError"));
+
+	worker->job.reset();
+	ui->progressBar->setVisible(false);
+	ui->remux->setEnabled(true);
+}
+
+RemuxWorker::RemuxWorker()
+{
+	os_event_init(&stop, OS_EVENT_TYPE_MANUAL);
+}
+
+RemuxWorker::~RemuxWorker()
+{
+	os_event_destroy(stop);
+}
+
+void RemuxWorker::UpdateProgress(float percent)
+{
+	if (abs(lastProgress - percent) < 0.1f)
+		return;
+
+	emit updateProgress(percent);
+	lastProgress = percent;
+}
+
+void RemuxWorker::remux()
+{
+	auto callback = [](void *data, float percent)
+	{
+		auto rw = static_cast<RemuxWorker*>(data);
+		rw->UpdateProgress(percent);
+		return !!os_event_try(rw->stop);
+	};
+
+	bool success = media_remux_job_process(job.get(), callback, this);
+
+	emit remuxFinished(os_event_try(stop) && success);
+}

+ 86 - 0
obs/window-remux.hpp

@@ -0,0 +1,86 @@
+/******************************************************************************
+    Copyright (C) 2014 by Ruwen Hahn <[email protected]>
+
+    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/>.
+******************************************************************************/
+
+#pragma once
+
+#include <QPointer>
+#include <QThread>
+#include <memory>
+#include "ui_OBSRemux.h"
+
+#include <media-io/media-remux.h>
+#include <util/threading.h>
+
+class RemuxWorker;
+
+class OBSRemux : public QDialog {
+	Q_OBJECT
+
+	QThread remuxer;
+	QPointer<RemuxWorker> worker;
+
+	std::unique_ptr<Ui::OBSRemux> ui;
+
+	const char *recPath;
+
+	void BrowseInput();
+	void BrowseOutput();
+	void Remux();
+
+	bool Stop();
+
+	virtual void closeEvent(QCloseEvent *event) override;
+	virtual void reject() override;
+
+public:
+	explicit OBSRemux(const char *recPath, QWidget *parent = nullptr);
+	virtual ~OBSRemux() override;
+
+	using job_t = std::shared_ptr<struct media_remux_job>;
+
+private slots:
+	void inputChanged(const QString &str);
+
+public slots:
+	void updateProgress(float percent);
+	void remuxFinished(bool success);
+
+signals:
+	void remux();
+};
+
+class RemuxWorker : public QObject {
+	Q_OBJECT
+
+	OBSRemux::job_t job;
+	os_event_t *stop;
+
+	float lastProgress;
+	void UpdateProgress(float percent);
+
+	explicit RemuxWorker();
+	virtual ~RemuxWorker();
+
+private slots:
+	void remux();
+
+signals:
+	void updateProgress(float percent);
+	void remuxFinished(bool success);
+
+	friend class OBSRemux;
+};