|
|
|
|
@ -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())); |
|
|
|
|
|