From df02aa81707701021dbf800a20894f2199cc867e Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 18 Oct 2026 22:06:34 +0200 Subject: [PATCH] Adapt the stream QP ceiling from measured encoded bitrate --- tools/chatgui/src/stream_publish_engine.cpp | 48 ++++++++++++++++++++- tools/chatgui/src/stream_publish_window.h | 3 ++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/tools/chatgui/src/stream_publish_engine.cpp b/tools/chatgui/src/stream_publish_engine.cpp index 2b777ade..1f70eedd 100644 --- a/tools/chatgui/src/stream_publish_engine.cpp +++ b/tools/chatgui/src/stream_publish_engine.cpp @@ -1,6 +1,7 @@ #include "stream_publish_engine.h" #include "sound_manager.h" #include "audio_event_queue.h" +#include "stream_qp_control.h" #include "../transport/stream_bridge.h" #include "debug_config.h" #include @@ -111,6 +112,7 @@ struct StreamPublishEngine::Impl { std::atomic imageFrames{0}; std::atomic convertedFrames{0}, encodedFrames{0}, deliveredFrames{0}, repeatedFrames{0}; std::atomic encodedBytes{0}, lastKeyBytes{0}; // Видео до сетевой отправки, без повторов транспорта. + std::atomic videoMaxQp{-1}; uint64_t bitrateBytes = 0, videoBitrate = 0; int64_t bitrateAt = 0; void videoProgress(const char* stage) { @@ -240,6 +242,7 @@ bool StreamPublishEngine::running() const { return m->running.load(); } uint64_t StreamPublishEngine::group() const { return m->group; } StreamPublishConfig StreamPublishEngine::config() const { std::lock_guard guard(m->lock); return m->wanted; } uint64_t StreamPublishEngine::videoBitrate() const { return active() ? m->videoBitrate : 0; } +int StreamPublishEngine::videoMaxQp() const { return active() ? m->videoMaxQp.load() : -1; } QVariant StreamPublishEngine::previewFrame() const { #ifdef HAVE_STREAM_CAPTURE std::lock_guard guard(m->lock); return QVariant::fromValue(m->previewFrame); @@ -580,6 +583,10 @@ bool StreamPublishEngine::start(uint64_t group, const StreamPublishConfig& confi DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop capture/encoder unavailable codec=%d", config.video.codec); emit errorOccurred(text("Video capture or encoder is unavailable")); return false; } + if (config.adaptiveQp && (config.video.maxQp < 0 || config.video.codec > 2)) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop invalid QP adaptation codec=%d max_qp=%d", config.video.codec, config.video.maxQp); + emit errorOccurred(text("Select a maximum QP before enabling adaptation")); return false; + } if (config.audio && !m_microphone.acquire()) { DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop publication blocked: microphone owned by another operation"); emit errorOccurred(text("The microphone is used by another operation")); return false; @@ -590,6 +597,7 @@ bool StreamPublishEngine::start(uint64_t group, const StreamPublishConfig& confi m->origin = m->started = monotonicUs(); m->stats = {}; } m->convertedFrames = m->encodedFrames = m->deliveredFrames = m->repeatedFrames = 0; m->encodedBytes = m->lastKeyBytes = 0; m->bitrateBytes = m->videoBitrate = 0; m->bitrateAt = m->origin; + m->videoMaxQp = config.video.maxQp; m->videoProgress("starting"); m->captureReport = 0; m->captureReportRaw = m->captureReportConverted = m->captureReportEncoded = m->captureReportDelivered = 0; m->id = QRandomGenerator::global()->generate64(); if (!m->id) m->id = 1; @@ -633,6 +641,10 @@ bool StreamPublishEngine::start(uint64_t group, const StreamPublishConfig& confi bool StreamPublishEngine::reconfigure(const StreamPublishConfig& config) { if (!m->running) return false; + if (config.adaptiveQp && (config.video.maxQp < 0 || config.video.codec > 2)) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop invalid QP adaptation codec=%d max_qp=%d", config.video.codec, config.video.maxQp); + emit errorOccurred(text("Select a maximum QP before enabling adaptation")); return false; + } StreamPublishConfig previous = this->config(); if (previous.source.id == "portal" && config.source.sameSource(previous.source) && config.showCursor != previous.showCursor) { DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "Wayland capture cursor mode cannot change during a portal session"); @@ -852,6 +864,7 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) { std::unique_ptr encoder; StreamPublishConfig encoding; QImage lastImage; uint64_t current = 0; uint32_t revision = 0; + StreamQpControl qp; uint64_t qpGeneration = 0; int64_t lastPts = -1, lastKey = -1000000, next = 0, report = 0; unsigned count = 0, skipped = 0; while (m->running) { m->videoProgress("waiting-frame"); @@ -892,7 +905,8 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) { if (generation != current) { auto comparable = config.video; comparable.bitrate = encoding.video.bitrate; bool onlyBitrate = encoder && comparable == encoding.video && config.source.sameSource(encoding.source); - if (onlyBitrate && encoder->setBitrate(config.video.bitrate)) { + if (onlyBitrate && (!config.adaptiveQp || encoder->canSetMaxQp()) + && (config.video == encoding.video || encoder->setBitrate(config.video.bitrate))) { current = generation; encoding = config; { std::lock_guard guard(m->lock); if (m->generation == generation) { m->good = config; m->hasGood = true; m->confirmedGeneration = generation; } } @@ -936,6 +950,13 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) { }); continue; } + if (config.adaptiveQp && !candidate->canSetMaxQp()) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop QP adaptation unavailable encoder=%s", qPrintable(candidate->name())); + QMetaObject::invokeMethod(this, [this, generation] { + sourceFailed(generation, text("QP adaptation is unavailable for this encoder. Disable adaptation or choose another codec.")); + }); + next = monotonicUs() + 100000; continue; + } // Начальный формат publisher уже зарегистрирован; уточнение размера требует новой revision даже до первого KEY. revision = revision ? revision + 1 : config.video == initial.video ? 1 : 2; m->videoProgress("replace-encoder"); @@ -948,6 +969,17 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) { (unsigned long long)generation, revision, qPrintable(encoder->name())); } } + if (qpGeneration != current) { + qp.reset(config.video.maxQp, config.video.bitrate, config.adaptiveQp, monotonicUs()); + if (encoder->canSetMaxQp() && !encoder->setMaxQp(config.video.maxQp < 0 ? 51 : config.video.maxQp)) { + QString error = encoder->error(); + QMetaObject::invokeMethod(this, [this, generation, error] { sourceFailed(generation, error); }); + next = monotonicUs() + 100000; continue; + } + qpGeneration = current; m->videoMaxQp = qp.limit(); + DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop QP control encoder=%s base=%d adaptive=%d target_bps=%d step=1 interval_ms=2000", + qPrintable(encoder->name()), config.video.maxQp, config.adaptiveQp, config.video.bitrate); + } bool screen = config.source.kind != StreamVideoSource::Camera; if (!frame.isNull()) { if (!screen && (capture <= lastPts || monotonicUs() - m->origin - capture > 250000)) { skipped++; continue; } @@ -968,9 +1000,11 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) { } m->encodedFrames += output.size(); m->videoProgress("publish-frame"); + uint64_t frameBytes = 0; stream_media_format format{(uint16_t)config.video.width, (uint16_t)config.video.height, (uint16_t)config.video.rotation, (uint8_t)config.video.codec, (uint8_t)(config.audio ? STREAM_MEDIA_OPUS_STEREO : 0), revision}; for (const auto& encoded : output) { + frameBytes += encoded.data.size(); m->encodedBytes += encoded.data.size(); if (encoded.key) m->lastKeyBytes = encoded.data.size(); if (gui_stream_publish_feed(m->id, true, encoded.pts, encoded.key, encoded.data, &format) == 1) { @@ -994,6 +1028,18 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) { } } else skipped++; } + const int previousQp = qp.limit(), selectedQp = qp.update(frameBytes, monotonicUs()); + if (selectedQp != previousQp) { + if (!encoder->setMaxQp(selectedQp)) { + QString error = encoder->error(); + QMetaObject::invokeMethod(this, [this, generation, error] { sourceFailed(generation, error); }); + next = monotonicUs() + 100000; continue; + } + m->videoMaxQp = selectedQp; + DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop QP adapted encoder=%s previous=%d selected=%d base=%d actual_bps=%llu target_bps=%d", + qPrintable(encoder->name()), previousQp, selectedQp, config.video.maxQp, + (unsigned long long)qp.bitrate(), config.video.bitrate); + } if (now - report >= 1000000) { DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop capture generation=%llu encoded=%u skipped=%u frame_age=%lldus encoder=%s", (unsigned long long)generation, count, skipped, (long long)(screen ? 0 : now - m->origin - capture), qPrintable(encoder->name())); diff --git a/tools/chatgui/src/stream_publish_window.h b/tools/chatgui/src/stream_publish_window.h index 3322d309..e0f17846 100644 --- a/tools/chatgui/src/stream_publish_window.h +++ b/tools/chatgui/src/stream_publish_window.h @@ -8,6 +8,7 @@ class QLabel; class QPushButton; class QCheckBox; class QButtonGroup; +class QSpinBox; class StreamPublishWindow : public QWidget { Q_OBJECT public: @@ -21,6 +22,8 @@ private: QList m_modes; QComboBox *m_source, *m_microphone, *m_mode, *m_codec, *m_bitrate; QCheckBox *m_audio, *m_mute, *m_cursor; + QSpinBox* m_maxQp; + QCheckBox* m_adaptiveQp; QLabel *m_status, *m_stats, *m_inputChannels; QPushButton* m_start; QButtonGroup* m_rotation;