#include #include #include #include #include #include #include #include #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #include #endif #include "ultragrid_window.hpp" #include "debug.hpp" #ifdef __APPLE__ #include #endif #include "utils/string_view_utils.hpp" namespace { QString argListToString(const QStringList& argList){ return argList.join(" "); } QStringList argStringToList(const QString& argString){ QStringList res; QString arg; bool escaped = false; for(int i = 0; i < argString.size(); i++){ if(argString[i] == '"'){ escaped = !escaped; continue; } if(argString[i] == ' ' && !escaped){ if(!arg.isEmpty()){ res.append(std::move(arg)); arg = ""; } } else { arg.append(argString[i]); } } if(!arg.isEmpty()){ res.append(std::move(arg)); } return res; } QStringList argStringToList(const std::string& argString){ return argStringToList(QString::fromStdString(argString)); } } //anonymous namespace void UltragridWindow::initializeUgOpts(){ ultragridExecutable = UltragridWindow::findUltragridExecutable(); previewTimer.setSingleShot(true); connect(&previewTimer, SIGNAL(timeout()), this, SLOT(updatePreview())); setupPreviewCallbacks(); settingsUi.init(&settings, &availableSettings); settingsUi.initMainWin(&ui); settingsWindow.init(&settingsUi); connectSignals(); setArgs(); ui.displayPreview->setKey("ug_preview_disp_unix" + settings.getSessRndKey()); ui.displayPreview->start(); ui.capturePreview->setKey("ug_preview_cap_unix" + settings.getSessRndKey()); ui.capturePreview->start(); QStringList args = QCoreApplication::arguments(); int index = args.indexOf("--config"); if(index != -1 && args.size() >= index + 1) { QString filename = args.at(index + 1); loadSettingsFile(filename); } launchQuery(); setupPreviewCallbacks(); previewStatus.setText("Preview: Stopped"); processStatus.setText("UG: Stopped"); ui.statusbar->addWidget(&processStatus); ui.statusbar->addWidget(&previewStatus); } UltragridWindow::UltragridWindow(QWidget *parent): QMainWindow(parent){ ui.setupUi(this); //ui.terminal->setVisible(false); #ifdef DEBUG ui.actionTest->setVisible(true); #endif //DEBUG QString verString(GIT_CURRENT_SHA1 "/" GIT_CURRENT_BRANCH); versionLabel.setText(QString("Ver: ") + verString); receiverLoss.reset(); ui.localPreviewBar->addWidget(&rtcpRr); ui.localPreviewBar->setAlignment(&rtcpRr, Qt::AlignBottom); ui.remotePreviewBar->addWidget(&receiverLoss); ui.remotePreviewBar->setAlignment(&receiverLoss, Qt::AlignBottom); ui.statusbar->addPermanentWidget(&versionLabel); ui.vuMeter->setControlPort(&controlPort); ui.vuMeter->setOnRightSide(true); ui.vuMeterSend->setControlPort(&controlPort); ui.vuMeterSend->setParsePrefix("stats ASEND"); using namespace std::placeholders; controlPort.addLineCallback(std::bind(&BandwidthWidget::parseLine, ui.send_bandwidth, _1)); controlPort.addLineCallback(std::bind(&UltragridWindow::recvFmtCallback, this, _1)); } void UltragridWindow::clearFmtLabels(){ ui.recvVideoFmtLabel->setText(""); ui.recvAudioFmtLabel->setText(""); }; void UltragridWindow::recvFmtCallback(std::string_view line){ static constexpr std::string_view prefix = "stats "; if(!sv_is_prefix(line, prefix)) return; line.remove_prefix(prefix.size()); static constexpr std::string_view videoFmt = "new incoming video fmt: "; static constexpr std::string_view audioFmt = "new incoming audio fmt: "; if(sv_is_prefix(line, videoFmt)){ line.remove_prefix(videoFmt.size()); std::string fmt(sv_trim_whitespace_back(line)); ui.recvVideoFmtLabel->setText(fmt.c_str()); } else if(sv_is_prefix(line, audioFmt)){ line.remove_prefix(audioFmt.size()); std::string fmt(sv_trim_whitespace_back(line)); ui.recvAudioFmtLabel->setText(fmt.c_str()); } } void UltragridWindow::launchQuery(){ auto ctx = std::make_unique(); ctx->executablePath = ultragridExecutable; ctx->args = QStringList() << "--capabilities"; ctx->type = LaunchContext::Type::Query; struct QueryData : public LaunchContext::ExtraData{ QLabel queryMsg = QLabel("Querying UltraGrid capabilities..."); bool safeMode = false; std::string currentQuery; std::vector safeModeQueue; std::vector safeModeFails; }; ctx->extraData = std::make_unique(); auto extraPtr = static_cast(ctx->extraData.get()); ui.statusbar->addWidget(&extraPtr->queryMsg); availableSettings.queryBegin(); connect(&ctx->process, &QProcess::started, [=]() { ui.startButton->setEnabled(false); ui.startButton->setText("Querying..."); }); connect(ctx.get(), &LaunchContext::processTerminated, [=](int ec, QProcess::ExitStatus es, bool requested) { ui.startButton->setEnabled(true); ui.startButton->setText("Start"); if(!requested && (es == QProcess::ExitStatus::CrashExit || ec != 0)){ if(!extraPtr->safeMode){ extraPtr->safeMode = true; ui.statusbar->showMessage("Capabilities querying in safe mode..."); for(int i = 0; i < SettingType::SETTING_TYPE_COUNT; i++){ SettingType type = static_cast(i); for(const auto& mod : availableSettings.getAvailableSettings(type)){ std::string item = settingTypeToStr(type); item += ":"; item += mod; extraPtr->safeModeQueue.push_back(std::move(item)); } } extraPtr->safeModeQueue.emplace_back("noprobe"); availableSettings.queryBegin(); } else { extraPtr->safeModeFails.push_back(extraPtr->currentQuery); } } if(extraPtr->safeMode && !extraPtr->safeModeQueue.empty()){ auto next_ctx = launchMngr.extractCurrentCtx(); auto modToQuery = std::move(extraPtr->safeModeQueue.back()); extraPtr->safeModeQueue.pop_back(); extraPtr->currentQuery = "--capabilities=" + modToQuery; next_ctx->args = QStringList() << QString::fromStdString(extraPtr->currentQuery); availableSettings.queryBeginPass(); launchMngr.reLaunch(std::move(next_ctx)); return; } if(extraPtr->safeMode && !extraPtr->safeModeFails.empty()){ QMessageBox msgBox; std::string msg = "Ultragrid crashed when querying the following modules." " Some options may be missing as a result.\n"; for(const auto& i : extraPtr->safeModeFails){ msg += "\n"; msg += i; } msgBox.setText(QString::fromStdString(msg)); msgBox.setIcon(QMessageBox::Warning); msgBox.exec(); } availableSettings.queryEnd(); settings.populateSettingsFromCapabilities(&availableSettings); settingsUi.refreshAll(); checkPreview(); launchPreview(); }); connect(ctx.get(), &LaunchContext::processOutputLine, [=](std::string_view line) { availableSettings.queryLine(line); }); launchMngr.launch(std::move(ctx)); } void UltragridWindow::refresh(){ launchQuery(); } void UltragridWindow::setupPreviewCallbacks(){ const char * const opts[] = { "video.source", "audio.source", "audio.source.channels", "encryption", "extraOpts" }; for(const auto &i : opts){ settings.getOption(i).addOnChangeCallback( Option::Callback(&UltragridWindow::schedulePreview, this)); } } void UltragridWindow::checkPreview(){ bool enable = true; if(!availableSettings.isAvailable("multiplier", VIDEO_DISPLAY) || !availableSettings.isAvailable("preview", VIDEO_DISPLAY)) { enable = false; QMessageBox warningBox(this); warningBox.setWindowTitle("Preview disabled"); warningBox.setText("Preview is disabled, because UltraGrid was compiled" " without preview and multiplier displays." " Please build UltraGrid configured with the --enable-qt flag" " to enable preview."); warningBox.setStandardButtons(QMessageBox::Ok); warningBox.setIcon(QMessageBox::Warning); warningBox.exec(); } QOpenGLContext ctx; ctx.create(); QOpenGLFunctions_3_3_Core* funcs = #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) ctx.versionFunctions(); #else QOpenGLVersionFunctionsFactory::get(&ctx); #endif if (!funcs) { enable = false; QMessageBox warningBox(this); warningBox.setWindowTitle("Preview disabled"); warningBox.setText("OpenGL 3.3 is not supported, disabling preview."); warningBox.setStandardButtons(QMessageBox::Ok); warningBox.setIcon(QMessageBox::Warning); warningBox.exec(); } if(!enable){ ui.previewCheckBox->setChecked(false); ui.previewCheckBox->setEnabled(false); settings.getOption("preview").setValue("f"); } } void UltragridWindow::about(){ QMessageBox aboutBox(this); aboutBox.setWindowTitle("About UltraGrid"); aboutBox.setTextFormat(Qt::RichText); aboutBox.setText( "UltraGrid from CESNET is a software " "implementation of high-quality low-latency video and audio transmissions using commodity PC and Mac hardware.

" "More information can be found at http://www.ultragrid.cz

" "Please read documents distributed with the product to find out current and former developers.

" "UltraGrid uses NDI®, NDI® is a registered trademark of Vizrt NDI AB" ); aboutBox.setStandardButtons(QMessageBox::Ok); aboutBox.exec(); } void UltragridWindow::startStop(){ if(launchMngr.getCurrentStatus() == LaunchContext::Type::Run){ ui.startButton->setText("Stopping..."); processStatus.setText("UG: Stopping"); ui.startButton->setEnabled(false); launchMngr.stop(); } else { start(); } } void UltragridWindow::start(){ log.write("Command args: " + argListToString(launchArgs) + "\n\n"); auto ctx = std::make_unique(); ctx->executablePath = ultragridExecutable; ctx->args = launchArgs; ctx->type = LaunchContext::Type::Run; clearFmtLabels(); connect(&ctx->process, &QProcess::started, [=]() { ui.startButton->setText("Stop"); ui.startButton->setEnabled(true); processStatus.setText("UG: Running"); ui.actionRefresh->setEnabled(false); }); connect(ctx.get(), &LaunchContext::processTerminated, [=](int ec, QProcess::ExitStatus es, bool requested) { ui.startButton->setText("Start"); ui.startButton->setEnabled(true); ui.actionRefresh->setEnabled(true); receiverLoss.reset(); clearFmtLabels(); rtcpRr.reset(); ui.send_bandwidth->reset(); if(!requested && (es == QProcess::ExitStatus::CrashExit || ec != 0)){ processStatus.setText("UG: Crashed"); log.write("Process exited with code: " + QString::number(ec) + "\n\n"); QMessageBox msgBox(this); msgBox.setIcon(QMessageBox::Critical); msgBox.setWindowTitle("UltraGrid error!"); msgBox.setText("Ultragrid has exited with an error! If you need help, please send an email " "to ultragrid-dev@cesnet.cz with log attached."); QPushButton *showLogBtn = msgBox.addButton(tr("Show log"), QMessageBox::ActionRole); msgBox.addButton(tr("Dismiss"), QMessageBox::RejectRole); msgBox.exec(); if(msgBox.clickedButton() == showLogBtn){ showLog(); } } else { processStatus.setText("UG: Stopped"); launchPreview(); } }); connect(ctx.get(), &LaunchContext::processOutputRead, [=](QString str) { log.write(str); }); connect(ctx.get(), &LaunchContext::processOutputLine, [=](std::string_view sv) { rtcpRr.parseLine(sv); controlPort.parseLine(sv); receiverLoss.parseLine(sv); }); ui.startButton->setText("Starting..."); processStatus.setText("UG: Starting"); ui.startButton->setEnabled(false); launchMngr.launch(std::move(ctx)); } void UltragridWindow::schedulePreview(Option&, bool, void *opaque){ UltragridWindow *obj = static_cast(opaque); obj->previewTimer.start(200); } void UltragridWindow::updatePreview(){ if(launchMngr.getCurrentStatus() == LaunchContext::Type::Run || launchMngr.getCurrentStatus() == LaunchContext::Type::Query) { return; } launchPreview(); } bool UltragridWindow::launchPreview(){ if(!ui.previewCheckBox->isEnabled() || !ui.previewCheckBox->isChecked()) { return false; } auto ctx = std::make_unique(); ctx->args = argStringToList(settings.getPreviewParams()); ctx->executablePath = ultragridExecutable; ctx->type = LaunchContext::Type::Preview; #ifdef DEBUG log.write("Preview: " + argListToString(ctx->args) + "\n\n"); #endif connect(&ctx->process, &QProcess::started, [=]() { previewStatus.setText("Preview: Running"); }); connect(ctx.get(), &LaunchContext::processTerminated, [=](int ec, QProcess::ExitStatus es, bool requested) { if(!requested && (es == QProcess::ExitStatus::CrashExit || ec != 0)){ previewStatus.setText("Preview: Crashed"); } else { previewStatus.setText("Preview: Stopped"); } }); connect(ctx.get(), &LaunchContext::processOutputLine, [=](std::string_view sv) { controlPort.parseLine(sv); }); launchMngr.launch(std::move(ctx)); return true; } void UltragridWindow::editArgs(const QString &text){ launchArgs = argStringToList(text); } void UltragridWindow::setArgs(){ QString args = QString::fromStdString(settings.getLaunchParams()); launchArgs = argStringToList(args); #ifdef DEBUG log.write("set args: " + args + "\n"); #endif ui.arguments->setText(args); } void UltragridWindow::closeEvent(QCloseEvent *e){ if(launchMngr.getCurrentStatus() == LaunchContext::Type::Run){ QMessageBox sureMsg; sureMsg.setIcon(QMessageBox::Question); sureMsg.setText(tr("Are you sure?")); sureMsg.setInformativeText(tr("UltraGrid is still running. Are you sure you want to exit?\n")); sureMsg.setStandardButtons(QMessageBox::Cancel | QMessageBox::No | QMessageBox::Yes); sureMsg.setDefaultButton(QMessageBox::No); if(sureMsg.exec() != QMessageBox::Yes){ e->ignore(); return; } } launchMngr.stop(); log.close(); settingsWindow.close(); } void UltragridWindow::connectSignals(){ connect(ui.actionAbout_UltraGrid, SIGNAL(triggered()), this, SLOT(about())); connect(ui.startButton, SIGNAL(clicked()), this, SLOT(startStop())); connect(ui.arguments, SIGNAL(textChanged(const QString &)), this, SLOT(editArgs(const QString &))); connect(ui.editCheckBox, SIGNAL(toggled(bool)), this, SLOT(setArgs())); //connect(ui.actionRefresh, SIGNAL(triggered()), this, SLOT(queryOpts())); connect(ui.actionShow_Terminal, SIGNAL(triggered()), this, SLOT(showLog())); connect(ui.actionSettings, SIGNAL(triggered()), this, SLOT(showSettings())); connect(ui.previewCheckBox, SIGNAL(toggled(bool)), this, SLOT(enablePreview(bool))); connect(ui.actionRefresh, SIGNAL(triggered()), this, SLOT(refresh())); connect(ui.actionSave, SIGNAL(triggered()), this, SLOT(saveSettings())); connect(ui.actionLoad, SIGNAL(triggered()), this, SLOT(loadSettings())); connect(&settingsWindow, SIGNAL(changed()), this, SLOT(setArgs())); connect(&settingsUi, SIGNAL(changed()), this, SLOT(setArgs())); connect(ui.fecToolBtn, SIGNAL(clicked()), this, SLOT(showSettings())); connect(ui.fecToolBtn, SIGNAL(clicked()), &settingsWindow, SLOT(fecTab())); } void UltragridWindow::showLog(){ log.show(); log.raise(); } void UltragridWindow::showSettings(){ settingsWindow.show(); settingsWindow.raise(); } void UltragridWindow::saveSettings(){ const auto &optMap = settings.getOptionMap(); QString filename = QFileDialog::getSaveFileName(this, "Save Settings", "", "Json (*.json)"); if(!filename.endsWith(QString(".json"))){ filename.append(QString(".json")); } QFile outFile(filename); if (!outFile.open(QIODevice::WriteOnly)) { qWarning("Couldn't open save file."); return; } QJsonObject jsonObj; for(const auto &keyValPair : optMap){ jsonObj[QString::fromStdString(keyValPair.first)] = QString::fromStdString(keyValPair.second->getValue()); } QJsonDocument jsonDoc(jsonObj); outFile.write(jsonDoc.toJson()); } void UltragridWindow::loadSettingsFile(const QString &filename){ QFile inFile(filename); if (!inFile.open(QIODevice::ReadOnly)) { qWarning("Couldn't open save file."); return; } QByteArray saveData = inFile.readAll(); QJsonDocument saveDoc = QJsonDocument::fromJson(saveData); QJsonObject saveObj = saveDoc.object(); for(const QString &key : saveObj.keys()){ if(saveObj[key].isString()){ settings.getOption(key.toStdString()) .setValue(saveObj[key].toString().toStdString(), true); } } settings.changedAll(); setArgs(); } void UltragridWindow::loadSettings(){ QFileDialog fileDialog(this); fileDialog.setFileMode(QFileDialog::ExistingFile); fileDialog.setNameFilter(tr("Json (*.json)")); QStringList fileNames; if (fileDialog.exec()) fileNames = fileDialog.selectedFiles(); if(fileNames.empty()) return; loadSettingsFile(fileNames.first()); } void UltragridWindow::enablePreview(bool enable){ if(enable) updatePreview(); else{ if(launchMngr.getCurrentStatus() == LaunchContext::Type::Preview){ launchMngr.stop(); } } } QString UltragridWindow::findUltragridExecutable() { QStringList args = QCoreApplication::arguments(); int index = args.indexOf("--with-uv"); if(index != -1 && args.size() >= index + 1) { //found return args.at(index + 1); } else { #ifdef __APPLE__ CFBundleRef mainBundle = CFBundleGetMainBundle(); CFURLRef cfURL = CFBundleCopyBundleURL(mainBundle); CFStringRef cfPath = CFURLCopyFileSystemPath(cfURL, kCFURLPOSIXPathStyle); CFStringEncoding encodingMethod = CFStringGetSystemEncoding(); QString bundlePath(CFStringGetCStringPtr(cfPath, encodingMethod)); CFRelease(cfURL); CFRelease(cfPath); return bundlePath + "/Contents/MacOS/uv"; #else return QString("uv"); #endif } } // vim: set noet: