Adapt the stream QP ceiling from measured encoded bitrate
This commit is contained in:
1 parent
0070221045
commit
df02aa8170
2 files changed
+50
-1
No files matched your search
@@ -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 <QGuiApplication>
|
||||
@@ -111,6 +112,7 @@ struct StreamPublishEngine::Impl {
|
||||
std::atomic<uint64_t> imageFrames{0};
|
||||
std::atomic<uint64_t> convertedFrames{0}, encodedFrames{0}, deliveredFrames{0}, repeatedFrames{0};
|
||||
std::atomic<uint64_t> encodedBytes{0}, lastKeyBytes{0}; // Видео до сетевой отправки, без повторов транспорта.
|
||||
std::atomic<int> 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<std::mutex> 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<std::mutex> 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<LiveVideoEncoder> 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<std::mutex> 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()));
|
||||
|
||||
Reference in new issue
Block a user