diff --git a/tools/chatgui/src/stream_publish_window.cpp b/tools/chatgui/src/stream_publish_window.cpp index fa4e6b97..02d7f9e3 100644 --- a/tools/chatgui/src/stream_publish_window.cpp +++ b/tools/chatgui/src/stream_publish_window.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -86,6 +87,15 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g 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")); @@ -102,9 +112,12 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g 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 @@ -143,6 +156,7 @@ StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t g 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))); }); @@ -176,6 +190,9 @@ void StreamPublishWindow::restoreSettings() { 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)); @@ -201,6 +218,7 @@ void StreamPublishWindow::saveSettings() { 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); @@ -291,6 +309,10 @@ StreamPublishConfig StreamPublishWindow::selection() const { 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; } @@ -320,11 +342,17 @@ void StreamPublishWindow::synchronize() { 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()); diff --git a/tools/chatgui/translations/chatgui_en.ts b/tools/chatgui/translations/chatgui_en.ts index 5d748c3f..2b67e926 100644 --- a/tools/chatgui/translations/chatgui_en.ts +++ b/tools/chatgui/translations/chatgui_en.ts @@ -3,6 +3,13 @@ ChatGui + Maximum QPMaximum QP + Adapt QP to the target bitrateAdapt QP to the target bitrate + Current QP ceiling: %1Current QP ceiling: %1 + Lower QP preserves more detail and may require a higher bitrate. Applies to H.264 and H.265.Lower QP preserves more detail and may require a higher bitrate. Applies to H.264 and H.265. + Gradually raise the QP ceiling above the selected value when bitrate is too high, then return it. Requires live QP adjustment.Gradually raise the QP ceiling above the selected value when bitrate is too high, then return it. Requires live QP adjustment. + Select a maximum QP before enabling adaptationSelect a maximum QP before enabling adaptation + QP adaptation is unavailable for this encoder. Disable adaptation or choose another codec.QP adaptation is unavailable for this encoder. Disable adaptation or choose another codec. Local networkLocal network Receive invitations from the local networkReceive invitations from the local network Broadcast presence on the local networkBroadcast presence on the local network