#include "stream_publish_window.h" #include "sound_manager.h" #include "debug_config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_STREAM_CAPTURE #include #include #include #endif namespace { constexpr int MaximumManualBitrate = 12000000; QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); } QString bytes(uint64_t value) { return QString::number(value / 1024.0, 'f', 1) + " KiB"; } } StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t group, QWidget* parent, const QString& saved) : QWidget(parent, Qt::Window), m_engine(engine), m_group(group) { if (!saved.isEmpty()) { QJsonParseError error; auto document = QJsonDocument::fromJson(saved.toUtf8(), &error); if (error.error != QJsonParseError::NoError || !document.isObject()) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop capture settings invalid: %s", qPrintable(error.errorString())); else m_settings = document.object(); } setWindowTitle(text("Broadcast stream")); resize(720, 720); setMinimumSize(460, 480); auto* escape = new QShortcut(QKeySequence(Qt::Key_Escape), this); escape->setContext(Qt::WidgetWithChildrenShortcut); connect(escape, &QShortcut::activated, this, &QWidget::close); auto* layout = new QVBoxLayout(this); #ifdef HAVE_STREAM_CAPTURE auto* preview = new QVideoWidget(this); m_preview = preview; preview->setMinimumHeight(160); preview->setAspectRatioMode(Qt::KeepAspectRatio); preview->videoSink()->setVideoFrame(engine->previewFrame().value()); connect(engine, &StreamPublishEngine::previewVideoChanged, this, [this, preview](const QVariant& frame) { auto video = frame.value(); if (!video.isValid() || isVisible()) preview->videoSink()->setVideoFrame(video); }); #else m_preview = new QLabel(text("Video capture requires Qt Multimedia and FFmpeg"), this); #endif layout->addWidget(m_preview, 1); auto* scroll = new QScrollArea(this); scroll->setWidgetResizable(true); auto* settings = new QWidget(scroll); auto* form = new QFormLayout(settings); form->setRowWrapPolicy(QFormLayout::WrapLongRows); scroll->setWidget(settings); scroll->setMinimumHeight(220); layout->addWidget(scroll); m_source = new QComboBox(settings); m_source->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); m_source->setMinimumContentsLength(16); m_source->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); auto* sourceRow = new QWidget(settings); auto* sourceLayout = new QHBoxLayout(sourceRow); sourceLayout->setContentsMargins(0, 0, 0, 0); sourceLayout->addWidget(m_source, 1); auto* refresh = new QPushButton(text("Refresh"), settings); sourceLayout->addWidget(refresh); connect(refresh, &QPushButton::clicked, this, &StreamPublishWindow::refreshSources); form->addRow(text("Video source"), sourceRow); connect(m_source, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { m_source->setToolTip(m_source->currentText()); }); m_mode = new QComboBox(settings); m_mode->setObjectName(QStringLiteral("streamVideoMode")); m_source->setObjectName(QStringLiteral("streamVideoSource")); form->addRow(text("Video mode"), m_mode); m_codec = new QComboBox(settings); form->addRow(text("Video codec"), m_codec); m_bitrate = new QComboBox(settings); m_bitrate->addItem(text("Automatic"), 0); for (int bitrate : {250000, 500000, 1000000, 2000000, 4000000, 8000000, MaximumManualBitrate}) m_bitrate->addItem(QStringLiteral("%1 Mbit/s").arg(bitrate / 1000000.0, 0, 'f', 2), bitrate); DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop manual stream bitrate selection maximum_bps=%d", MaximumManualBitrate); form->addRow(text("Video bitrate"), m_bitrate); m_rotation = new QButtonGroup(this); auto* rotationRow = new QWidget(settings); auto* angles = new QHBoxLayout(rotationRow); angles->setContentsMargins(0, 0, 0, 0); for (int angle : {0, 90, 270}) { auto* button = new QPushButton(QStringLiteral("%1°").arg(angle), settings); button->setCheckable(true); button->setFixedSize(66, 34); QPixmap icon(20, 20); icon.fill(Qt::transparent); QPainter painter(&icon); painter.translate(10, 10); painter.rotate(-angle); painter.setPen(QPen(QColor("#e53935"), 3)); painter.drawLine(-6, -6, 6, -6); painter.setPen(QPen(palette().color(QPalette::Text), 1)); painter.drawLine(-6, -4, -6, 6); painter.drawLine(6, -4, 6, 6); painter.drawLine(-6, 6, 6, 6); painter.end(); button->setIcon(QIcon(icon)); button->setToolTip(text("The red edge marks the top for viewers")); button->setStyleSheet("QPushButton:checked { background: #1976d2; color: white; }"); m_rotation->addButton(button, angle); angles->addWidget(button); } m_rotation->button(0)->setChecked(true); angles->addStretch(); form->addRow(text("Rotation"), rotationRow); m_audio = new QCheckBox(text("Microphone enabled"), settings); m_audio->setChecked(true); form->addRow(m_audio); m_cursor = new QCheckBox(text("Show mouse cursor"), settings); m_cursor->setChecked(true); m_cursor->setObjectName(QStringLiteral("streamShowCursor")); form->insertRow(2, m_cursor); m_microphone = new QComboBox(settings); m_microphone->addItem(text("Default microphone"), QString()); for (const auto& device : SoundManager::enumerateCaptureDevices()) m_microphone->addItem(device.name, device.id); form->addRow(text("Microphone"), m_microphone); m_inputChannels = new QLabel(settings); form->addRow(text("Audio input"), m_inputChannels); m_mute = new QCheckBox(text("Mute stream audio"), settings); form->addRow(m_mute); m_maxQp = new QSpinBox(settings); m_maxQp->setObjectName(QStringLiteral("streamMaxQp")); m_maxQp->setRange(0, 51); m_maxQp->setSpecialValueText(text("Automatic")); m_maxQp->setValue(28); m_maxQp->setToolTip(text("Lower QP preserves more detail and may require a higher bitrate. Applies to H.264 and H.265.")); m_adaptiveQp = new QCheckBox(text("Adapt QP to the target bitrate"), settings); m_adaptiveQp->setObjectName(QStringLiteral("streamAdaptiveQp")); m_adaptiveQp->setToolTip(text( "Gradually raise the QP ceiling above the selected value when bitrate is too high, then return it. Requires live QP adjustment.")); int bitrateRow = 0; QFormLayout::ItemRole bitrateRole; form->getWidgetPosition(m_bitrate, &bitrateRow, &bitrateRole); form->insertRow(bitrateRow + 1, text("Maximum QP"), m_maxQp); form->insertRow(bitrateRow + 2, m_adaptiveQp); connect(m_mute, &QCheckBox::toggled, engine, &StreamPublishEngine::setMuted); m_status = new QLabel(this); m_status->setWordWrap(true); layout->addWidget(m_status); m_stats = new QLabel(this); m_stats->setObjectName(QStringLiteral("streamPublishStats")); m_stats->setWordWrap(true); m_stats->setMinimumHeight(66); layout->addWidget(m_stats); m_start = new QPushButton(text("Start stream"), this); layout->addWidget(m_start); connect(m_start, &QPushButton::clicked, this, [this] { if (!m_engine->active()) m_status->setProperty("streamError", QString()); if (m_engine->active()) m_engine->stop(); else m_engine->start(m_group, selection()); synchronize(); }); connect(m_source, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { if (m_updating) return; auto previous = selection(); refreshModes(previous, previous.source.kind == StreamVideoSource::Screen); apply(); if (!m_engine->active()) synchronize(); }); for (auto* box : {m_mode, m_codec, m_bitrate}) connect(box, QOverload::of(&QComboBox::currentIndexChanged), this, &StreamPublishWindow::apply); connect(m_codec, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { if (!m_updating && !m_engine->active()) synchronize(); }); connect(m_microphone, QOverload::of(&QComboBox::currentIndexChanged), this, &StreamPublishWindow::apply); connect(m_audio, &QCheckBox::toggled, this, &StreamPublishWindow::apply); connect(m_cursor, &QCheckBox::toggled, this, &StreamPublishWindow::apply); connect(m_maxQp, QOverload::of(&QSpinBox::valueChanged), this, [this] { apply(); if (!m_engine->active()) synchronize(); }); connect(m_adaptiveQp, &QCheckBox::toggled, this, &StreamPublishWindow::apply); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) connect(m_rotation, &QButtonGroup::idClicked, this, &StreamPublishWindow::apply); #else connect(m_rotation, QOverload::of(&QButtonGroup::buttonClicked), this, &StreamPublishWindow::apply); #endif connect(engine, &StreamPublishEngine::codecsChanged, this, &StreamPublishWindow::refreshCodecs); connect(engine, &StreamPublishEngine::sourcesChanged, this, &StreamPublishWindow::refreshSources); connect(engine, &StreamPublishEngine::configurationChanged, this, [this] { synchronize(); saveSettings(); }); connect(engine, &StreamPublishEngine::stateChanged, this, [this](const QString& value) { auto error = m_status->property("streamError").toString(); m_status->setText(error.isEmpty() ? value : value + "\n" + error); synchronize(); }); connect(engine, &StreamPublishEngine::errorOccurred, this, [this](const QString& value) { m_status->setProperty("streamError", value); m_status->setText(value); }); connect(engine, &StreamPublishEngine::softwareEncodingRequested, this, [this](uint64_t generation, const QString& codec, const QString& details) { auto* question = new QMessageBox(QMessageBox::Warning, text("Hardware encoding is unavailable"), text("Hardware encoding for %1 is unavailable in the selected video mode. Check the graphics driver or choose another codec.\n\n" "Use CPU encoding instead? This may cause high CPU usage and slow down the computer.").arg(codec), QMessageBox::Yes | QMessageBox::No, this); question->setObjectName(QStringLiteral("streamCpuConfirmation")); question->setDefaultButton(QMessageBox::No); question->setEscapeButton(QMessageBox::No); question->setDetailedText(details); question->setAttribute(Qt::WA_DeleteOnClose); auto* engine = m_engine; connect(question, &QMessageBox::finished, engine, [engine, generation](int result) { engine->answerSoftwareEncoding(generation, result == QMessageBox::Yes); }); connect(question, &QObject::destroyed, engine, [engine, generation] { engine->answerSoftwareEncoding(generation, false); }, Qt::QueuedConnection); connect(engine, &StreamPublishEngine::stateChanged, question, [engine, generation, question] { if (engine->softwareEncodingRequest() != generation) question->reject(); }); question->open(); }); connect(engine, &StreamPublishEngine::audioChannelsChanged, this, [this](int channels) { m_inputChannels->setText(channels == 2 ? text("Stereo") : text("Mono")); }); connect(engine, &StreamPublishEngine::telemetryChanged, this, [this](const stream_publisher_status& stats) { m_stats->setText(text("Current video bitrate: %1 Mbit/s").arg(m_engine->videoBitrate() / 1000000.0, 0, 'f', 2) + '\n' + (m_engine->videoMaxQp() >= 0 ? text("Current QP ceiling: %1").arg(m_engine->videoMaxQp()) + '\n' : QString()) + text("Video queue: %1 · Inflight: %2 · Send buffer: %3\nACK, all connections, last second: %4") .arg(bytes(stats.video_bytes), bytes(stats.inflight_bytes), bytes(stats.total_bytes), bytes(stats.acked_1s))); }); refreshSources(); refreshCodecs(); restoreSettings(); synchronize(); } void StreamPublishWindow::closeEvent(QCloseEvent* event) { QWidget::closeEvent(event); if (event->isAccepted()) { DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop publisher controls closed group=%llx active=%d", (unsigned long long)m_group, m_engine->active()); emit closed(); } } /* В БД храним пользовательский выбор, а не runtime handles источника и рассчитанный auto-битрейт. */ void StreamPublishWindow::restoreSettings() { if (m_settings.isEmpty() || m_engine->active()) return; m_updating = true; auto select = [](QComboBox* box, const QVariant& value, const char* field) { int index = box->findData(value); if (index >= 0) box->setCurrentIndex(index); else DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop capture setting unavailable field=%s value=%s", field, qPrintable(value.toString())); }; int sourceIndex = -1; for (int i = 0; i < m_sources.size(); i++) { const auto& source = m_sources[i]; if (source.kind != m_settings.value("source_kind").toInt()) continue; bool matches = source.kind == StreamVideoSource::Window ? source.name == m_settings.value("source_name").toString() : source.id == QByteArray::fromBase64(m_settings.value("source_id").toString().toLatin1()); if (matches) { sourceIndex = i; break; } } if (sourceIndex >= 0) m_source->setCurrentIndex(sourceIndex); else DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved capture source unavailable; using current source"); auto restored = selection(); restored.video.width = m_settings.value("width").toInt(1280); restored.video.height = m_settings.value("height").toInt(720); restored.video.fps = m_settings.value("fps").toInt(30); restored.nativeSize = m_settings.value("native_size").toBool(); refreshModes(restored, sourceIndex < 0 && restored.source.kind == StreamVideoSource::Screen); int bitrate = m_settings.value("bitrate").toInt(); if (bitrate > MaximumManualBitrate) { DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved stream bitrate adjusted old_bps=%d maximum_bps=%d", bitrate, MaximumManualBitrate); bitrate = MaximumManualBitrate; } if (bitrate > 0 && m_bitrate->findData(bitrate) < 0) m_bitrate->addItem(QStringLiteral("%1 Mbit/s").arg(bitrate / 1000000.0, 0, 'f', 2), bitrate); select(m_bitrate, bitrate, "bitrate"); int maxQp = m_settings.value("max_qp").toInt(28); if (maxQp < 0 || maxQp > 51) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved QP ceiling invalid value=%d", maxQp); m_maxQp->setValue(std::clamp(maxQp, 0, 51)); m_adaptiveQp->setChecked(m_settings.value("adaptive_qp").toBool()); if (auto* button = m_rotation->button(m_settings.value("rotation").toInt())) button->setChecked(true); else DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved capture rotation invalid"); m_audio->setChecked(m_settings.value("audio").toBool(true)); m_cursor->setChecked(m_settings.value("show_cursor").toBool(true)); select(m_microphone, m_settings.value("microphone").toString(), "microphone"); m_updating = false; DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop capture settings restored: %s", QJsonDocument(m_settings).toJson(QJsonDocument::Compact).constData()); } void StreamPublishWindow::saveSettings() { if (m_updating) return; auto config = selection(); auto saved = m_settings; if (m_source->currentIndex() >= 0) { saved["source_kind"] = config.source.kind; saved["source_id"] = QString::fromLatin1(config.source.id.toBase64()); saved["source_name"] = config.source.kind == StreamVideoSource::Window ? config.source.name : QString(); } if (m_mode->currentIndex() >= 0) { saved["width"] = config.video.width; saved["height"] = config.video.height; saved["fps"] = config.video.fps; saved["native_size"] = config.nativeSize; } if (m_codec->currentIndex() >= 0) saved["codec"] = config.video.codec; saved["bitrate"] = m_bitrate->currentData().toInt(); saved["rotation"] = config.video.rotation; saved["audio"] = config.audio; saved["microphone"] = config.microphone; saved["show_cursor"] = config.showCursor; saved["max_qp"] = m_maxQp->value(); saved["adaptive_qp"] = m_adaptiveQp->isChecked(); if (saved == m_settings) return; m_settings = saved; auto json = QJsonDocument(saved).toJson(QJsonDocument::Compact); DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop capture settings selected: %s", json.constData()); emit settingsChanged(QString::fromUtf8(json)); } void StreamPublishWindow::refreshSources() { QScopedValueRollback updating(m_updating, true); auto config = m_engine->active() ? m_engine->config() : selection(); m_sources = StreamPublishEngine::sources(); m_source->clear(); int selected = -1; for (int i = 0; i < m_sources.size(); i++) { auto& source = m_sources[i]; if (source.sameSource(config.source)) { selected = i; if (m_engine->active() && !config.source.size.isEmpty()) source.size = config.source.size; } QString kind = source.kind == StreamVideoSource::Camera ? text("Camera") : source.kind == StreamVideoSource::Screen ? text("Screen") : text("Window"); QString label = kind + ": " + source.name; if (source.kind == StreamVideoSource::Screen && !source.size.isEmpty()) label += QStringLiteral(" (%1 × %2)").arg(source.size.width()).arg(source.size.height()); m_source->addItem(label); } bool changed = selected < 0; if (changed && !m_engine->active() && !m_sources.isEmpty()) selected = 0; m_source->setCurrentIndex(selected); refreshModes(config, changed && selected >= 0 && m_sources[selected].kind == StreamVideoSource::Screen); synchronize(); } /* Перестраиваем весь список под блокировкой, чтобы в engine не ушёл промежуточный режим другого источника. */ void StreamPublishWindow::refreshModes(const StreamPublishConfig& previous, bool maximum) { QScopedValueRollback updating(m_updating, true); m_mode->clear(); m_modes.clear(); int sourceIndex = m_source->currentIndex(); if (sourceIndex < 0 || sourceIndex >= m_sources.size()) return; const auto& source = m_sources[sourceIndex]; if (source.kind == StreamVideoSource::Camera) m_modes = source.modes; else if (source.kind == StreamVideoSource::Screen) m_modes = stream_video_screen_modes(source.size); else { for (QSize size : {QSize(1920, 1080), QSize(1280, 720), QSize(640, 360)}) for (int fps : {15, 30, 60}) m_modes.append({size, fps}); } for (const auto& mode : m_modes) { QString label = mode.size.isEmpty() ? text("Original resolution · %1 fps").arg(mode.fps) : QStringLiteral("%1 × %2 · %3 fps").arg(mode.size.width()).arg(mode.size.height()).arg(mode.fps); m_mode->addItem(label, QVariantList{mode.size.width(), mode.size.height(), mode.fps}); m_mode->setItemData(m_mode->count() - 1, source.kind == StreamVideoSource::Screen && mode.size == m_modes.front().size, Qt::UserRole + 1); } maximum |= source.kind == StreamVideoSource::Screen && previous.nativeSize; StreamVideoMode wanted{QSize(previous.video.width, previous.video.height), previous.video.fps}; int selected = stream_video_select_mode(m_modes, wanted, maximum); m_mode->setCurrentIndex(selected); if (selected < 0) { DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop source has no selectable modes source=%s", qPrintable(source.name)); m_mode->setToolTip(text("No supported video modes")); return; } const auto& mode = m_modes[selected]; if (!maximum && wanted != mode) DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop video mode adjusted source=%s requested=%dx%d/%d selected=%dx%d/%d", qPrintable(source.name), wanted.size.width(), wanted.size.height(), wanted.fps, mode.size.width(), mode.size.height(), mode.fps); m_mode->setToolTip(source.kind == StreamVideoSource::Camera ? text("Frame rate is limited by the camera and the selected stream mode") : QString()); DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop video modes source=%s count=%lld selected=%dx%d/%d native=%d", qPrintable(source.name), (long long)m_modes.size(), mode.size.width(), mode.size.height(), mode.fps, m_mode->currentData(Qt::UserRole + 1).toBool()); } void StreamPublishWindow::refreshCodecs() { m_updating = true; int previous = m_codec->currentIndex() >= 0 ? m_codec->currentData().toInt() : m_settings.value("codec").toInt(); m_codec->clear(); for (int codec : m_engine->codecs()) m_codec->addItem(LiveVideoEncoder::codecName(codec), codec); int index = m_codec->findData(previous); if (index >= 0) m_codec->setCurrentIndex(index); else if (previous && m_codec->count()) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved codec=%d unavailable; using codec=%d", previous, m_codec->currentData().toInt()); m_updating = false; synchronize(); } StreamPublishConfig StreamPublishWindow::selection() const { StreamPublishConfig config; if (m_source->currentIndex() >= 0 && m_source->currentIndex() < m_sources.size()) config.source = m_sources[m_source->currentIndex()]; auto mode = m_mode->currentData().toList(); if (mode.size() == 3) { if (mode[0].toInt() > 0 && mode[1].toInt() > 0) { config.video.width = mode[0].toInt(); config.video.height = mode[1].toInt(); } config.video.fps = mode[2].toInt(); } config.nativeSize = config.source.kind == StreamVideoSource::Screen && m_mode->currentData(Qt::UserRole + 1).toBool(); config.video.codec = m_codec->currentData().toInt(); config.video.rotation = m_rotation->checkedId(); config.video.bitrate = m_bitrate->currentData().toInt(); config.automaticBitrate = !config.video.bitrate; if (config.automaticBitrate) config.video.bitrate = std::clamp(config.video.width * config.video.height * config.video.fps / 11, 250000, 16000000); if (config.video.codec == 1 || config.video.codec == 2) { config.video.maxQp = m_maxQp->value() ? m_maxQp->value() : -1; config.adaptiveQp = config.video.maxQp >= 0 && m_adaptiveQp->isChecked(); } config.showCursor = m_cursor->isChecked(); config.microphone = m_microphone->currentData().toString(); config.audio = m_audio->isChecked(); return config; } void StreamPublishWindow::apply() { if (m_updating || m_mode->currentIndex() < 0) return; if (m_engine->active()) m_engine->reconfigure(selection()); saveSettings(); } void StreamPublishWindow::synchronize() { QScopedValueRollback updating(m_updating, true); bool active = m_engine->active(); m_start->setText(active ? text("Stop stream") : text("Start stream")); m_start->setEnabled((active && m_engine->running()) || (!active && StreamPublishEngine::supported() && m_source->currentIndex() >= 0 && m_mode->currentIndex() >= 0 && m_codec->count())); for (auto* box : {m_source, m_mode, m_codec, m_bitrate}) box->setEnabled(!active || m_engine->running()); m_audio->setEnabled(!active); m_microphone->setEnabled(!active); m_mute->setEnabled(m_engine->running() && m_engine->config().audio); { QSignalBlocker blocked(m_mute); m_mute->setChecked(m_engine->muted()); } if (active) { auto config = m_engine->config(); for (int i = 0; i < m_sources.size(); i++) if (m_sources[i].sameSource(config.source)) { m_source->setCurrentIndex(i); if (!config.source.size.isEmpty()) m_sources[i].size = config.source.size; } refreshModes(config); m_codec->setCurrentIndex(m_codec->findData(config.video.codec)); int bitrateIndex = config.automaticBitrate ? 0 : m_bitrate->findData(config.video.bitrate); if (bitrateIndex < 0) { m_bitrate->addItem(QStringLiteral("%1 Mbit/s").arg(config.video.bitrate / 1000000.0, 0, 'f', 2), config.video.bitrate); bitrateIndex = m_bitrate->count() - 1; } m_bitrate->setCurrentIndex(bitrateIndex); if (config.video.codec == 1 || config.video.codec == 2) { m_maxQp->setValue(config.video.maxQp < 0 ? 0 : config.video.maxQp); m_adaptiveQp->setChecked(config.adaptiveQp); } if (auto* button = m_rotation->button(config.video.rotation)) button->setChecked(true); m_audio->setChecked(config.audio); m_microphone->setCurrentIndex(m_microphone->findData(config.microphone)); m_cursor->setChecked(config.showCursor); } m_cursor->setVisible(StreamPublishEngine::cursorSupported(selection().source)); bool qpSupported = m_codec->currentData().toInt() == 1 || m_codec->currentData().toInt() == 2; m_maxQp->setEnabled(qpSupported && (!active || m_engine->running())); m_adaptiveQp->setEnabled(m_maxQp->isEnabled() && m_maxQp->value() > 0); bool portal = selection().source.id == "portal"; m_cursor->setEnabled(!active || (m_engine->running() && !portal)); m_cursor->setToolTip(portal ? text("Change cursor visibility before starting screen sharing") : QString()); }