|
|
|
|
@ -72,6 +72,12 @@ struct StreamPublishEngine::Impl {
|
|
|
|
|
int channels = 2; |
|
|
|
|
uint64_t capturedFrames = 0; |
|
|
|
|
int64_t captureReport = 0; |
|
|
|
|
std::atomic<const char*> videoStage{"stopped"}; |
|
|
|
|
std::atomic<int64_t> videoStageAt{0}; |
|
|
|
|
std::atomic<uint64_t> convertedFrames{0}, encodedFrames{0}, deliveredFrames{0}, repeatedFrames{0}; |
|
|
|
|
void videoProgress(const char* stage) { |
|
|
|
|
videoStageAt.store(monotonicUs()); videoStage.store(stage); |
|
|
|
|
} |
|
|
|
|
bool hasGood = false, rollingBack = false, videoReady = false; |
|
|
|
|
uint64_t softwareRequest = 0; |
|
|
|
|
int softwareCodec = 0; |
|
|
|
|
@ -385,6 +391,8 @@ bool StreamPublishEngine::start(uint64_t group, const StreamPublishConfig& confi
|
|
|
|
|
m->wanted = config; m->good = {}; m->hasGood = m->rollingBack = false; m->generation++; m->confirmedGeneration = 0; |
|
|
|
|
m->videoReady = false; m->softwareRequest = 0; m->softwareCodec = 0; m->lastAudioAt = 0; |
|
|
|
|
m->origin = m->started = monotonicUs(); m->stats = {}; } |
|
|
|
|
m->convertedFrames = m->encodedFrames = m->deliveredFrames = m->repeatedFrames = 0; |
|
|
|
|
m->videoProgress("starting"); m->captureReport = 0; |
|
|
|
|
m->id = QRandomGenerator::global()->generate64(); if (!m->id) m->id = 1; |
|
|
|
|
m->group = group; m->audio.reset(); m->audioSequence = 0; m->muted = false; |
|
|
|
|
stream_media_format format{(uint16_t)config.video.width, (uint16_t)config.video.height, (uint16_t)config.video.rotation, |
|
|
|
|
@ -456,10 +464,23 @@ void StreamPublishEngine::publicationFailed(uint64_t id, const QString& error) {
|
|
|
|
|
/* Сначала callbacks, затем workers; FIN не обгоняет уже принятые access units. */ |
|
|
|
|
void StreamPublishEngine::stop() { |
|
|
|
|
if (!m->running.exchange(false)) return; |
|
|
|
|
int64_t started = monotonicUs(); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop stop begin id=%llu video_stage=%s stage_age_ms=%lld converted=%llu encoded=%llu delivered=%llu", |
|
|
|
|
(unsigned long long)m->id, m->videoStage.load(), (long long)((started - m->videoStageAt.load()) / 1000), |
|
|
|
|
(unsigned long long)m->convertedFrames.load(), (unsigned long long)m->encodedFrames.load(), |
|
|
|
|
(unsigned long long)m->deliveredFrames.load()); |
|
|
|
|
{ std::lock_guard<std::mutex> guard(m->lock); m->softwareRequest = 0; } |
|
|
|
|
m->wake.notify_all(); closeSource(); m_capture.closeAsync(); |
|
|
|
|
m->wake.notify_all(); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop stop closing video source"); |
|
|
|
|
closeSource(); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop stop video source closed elapsed_ms=%lld", (long long)((monotonicUs() - started) / 1000)); |
|
|
|
|
m_capture.closeAsync(); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop stop joining video worker stage=%s stage_age_ms=%lld", |
|
|
|
|
m->videoStage.load(), (long long)((monotonicUs() - m->videoStageAt.load()) / 1000)); |
|
|
|
|
if (m->videoThread.joinable()) m->videoThread.join(); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop stop video joined; joining audio elapsed_ms=%lld", (long long)((monotonicUs() - started) / 1000)); |
|
|
|
|
if (m->audioThread.joinable()) m->audioThread.join(); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop stop workers joined; sending FIN elapsed_ms=%lld", (long long)((monotonicUs() - started) / 1000)); |
|
|
|
|
gui_stream_publish_stop(m->id); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop capture stopped id=%llu; draining publisher", (unsigned long long)m->id); |
|
|
|
|
emit stateChanged(text("Finishing stream…")); |
|
|
|
|
@ -586,6 +607,7 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) {
|
|
|
|
|
QImage lastImage; uint64_t current = 0; uint32_t revision = 0; |
|
|
|
|
int64_t lastPts = -1, lastKey = -1000000, next = 0, report = 0; unsigned count = 0, skipped = 0; |
|
|
|
|
while (m->running) { |
|
|
|
|
m->videoProgress("waiting-frame"); |
|
|
|
|
StreamPublishConfig config; uint64_t generation; QVideoFrame frame; int64_t capture; |
|
|
|
|
{ std::unique_lock<std::mutex> lock(m->lock); config = m->wanted; generation = m->generation; |
|
|
|
|
if (config.audio && !m->lastAudioAt.load(std::memory_order_acquire)) { |
|
|
|
|
@ -632,6 +654,7 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) {
|
|
|
|
|
}); |
|
|
|
|
} else { |
|
|
|
|
auto candidate = std::make_unique<LiveVideoEncoder>(); |
|
|
|
|
m->videoProgress("open-hardware-encoder"); |
|
|
|
|
bool opened = candidate->open(config.video, LiveVideoEncoder::Backend::HardwareOnly); |
|
|
|
|
if (!opened && LiveVideoEncoder::available(LiveVideoEncoder::Backend::SoftwareOnly).contains(config.video.codec)) { |
|
|
|
|
QString details = candidate->error(); |
|
|
|
|
@ -639,6 +662,7 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) {
|
|
|
|
|
if (!m->running || generation != m->generation) continue; |
|
|
|
|
if (m->softwareCodec != config.video.codec) { |
|
|
|
|
m->softwareRequest = generation; |
|
|
|
|
m->videoProgress("waiting-cpu-consent"); |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop CPU confirmation required generation=%llu codec=%d reason=%s", |
|
|
|
|
(unsigned long long)generation, config.video.codec, qPrintable(details)); |
|
|
|
|
QMetaObject::invokeMethod(this, [this, generation, codec = config.video.codec, details] { |
|
|
|
|
@ -652,6 +676,7 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) {
|
|
|
|
|
} |
|
|
|
|
if (!m->running || generation != m->generation) continue; |
|
|
|
|
lock.unlock(); |
|
|
|
|
m->videoProgress("open-software-encoder"); |
|
|
|
|
opened = candidate->open(config.video, LiveVideoEncoder::Backend::SoftwareOnly); |
|
|
|
|
} |
|
|
|
|
if (!opened) { |
|
|
|
|
@ -666,6 +691,7 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) {
|
|
|
|
|
} |
|
|
|
|
// Начальный формат publisher уже зарегистрирован; уточнение размера требует новой revision даже до первого KEY.
|
|
|
|
|
revision = revision ? revision + 1 : config.video == initial.video ? 1 : 2; |
|
|
|
|
m->videoProgress("replace-encoder"); |
|
|
|
|
encoder = std::move(candidate); current = generation; encoding = config; lastImage = {}; lastKey = -1000000; |
|
|
|
|
{ std::lock_guard<std::mutex> guard(m->lock); |
|
|
|
|
if (!m->running || generation != m->generation) continue; |
|
|
|
|
@ -678,7 +704,9 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) {
|
|
|
|
|
bool screen = config.source.kind != StreamVideoSource::Camera; |
|
|
|
|
if (frame.isValid()) { |
|
|
|
|
if (!screen && (capture <= lastPts || monotonicUs() - m->origin - capture > 250000)) { skipped++; continue; } |
|
|
|
|
m->videoProgress("convert-frame-to-image"); |
|
|
|
|
lastImage = frame.toImage(); |
|
|
|
|
m->convertedFrames++; |
|
|
|
|
if (lastImage.isNull()) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop captured frame conversion failed format=%d", frame.pixelFormat()); |
|
|
|
|
QMetaObject::invokeMethod(this, [this, generation] { sourceFailed(generation, text("Video frame conversion failed")); }); |
|
|
|
|
@ -692,20 +720,25 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) {
|
|
|
|
|
#endif |
|
|
|
|
} else if (!screen || lastImage.isNull()) { |
|
|
|
|
std::unique_lock<std::mutex> lock(m->lock); m->wake.wait_for(lock, std::chrono::milliseconds(5)); continue; |
|
|
|
|
} |
|
|
|
|
} else m->repeatedFrames++; |
|
|
|
|
m->videoProgress("publisher-status"); |
|
|
|
|
int64_t now = monotonicUs(), pts = screen ? now - m->origin : capture; |
|
|
|
|
next = now + 1000000 / config.video.fps; |
|
|
|
|
stream_publisher_status status; bool needsKey = gui_stream_publish_status(m->id, status) && status.need_key; |
|
|
|
|
bool key = pts - lastKey >= 1000000 || needsKey; |
|
|
|
|
QList<LiveEncodedFrame> output; |
|
|
|
|
m->videoProgress("encode-frame"); |
|
|
|
|
if (!encoder->encode(lastImage, std::max(lastPts + 1, pts), key, output)) { |
|
|
|
|
QString error = encoder->error(); QMetaObject::invokeMethod(this, [this, generation, error] { sourceFailed(generation, error); }); continue; |
|
|
|
|
} |
|
|
|
|
m->encodedFrames += output.size(); |
|
|
|
|
m->videoProgress("publish-frame"); |
|
|
|
|
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) { |
|
|
|
|
if (gui_stream_publish_feed(m->id, true, encoded.pts, encoded.key, encoded.data, &format) == 1) { |
|
|
|
|
lastPts = encoded.pts; if (encoded.key) lastKey = encoded.pts; count++; |
|
|
|
|
m->deliveredFrames++; |
|
|
|
|
if (encoded.key) { |
|
|
|
|
bool first; |
|
|
|
|
{ std::lock_guard<std::mutex> guard(m->lock); first = !m->hasGood || m->good.video != config.video || m->rollingBack; |
|
|
|
|
@ -729,7 +762,12 @@ void StreamPublishEngine::videoLoop(StreamPublishConfig initial) {
|
|
|
|
|
(unsigned long long)generation, count, skipped, (long long)(screen ? 0 : now - m->origin - capture), qPrintable(encoder->name())); |
|
|
|
|
report = now; count = skipped = 0; |
|
|
|
|
} |
|
|
|
|
m->videoProgress("release-frame"); |
|
|
|
|
} |
|
|
|
|
m->videoProgress("close-encoder"); |
|
|
|
|
encoder.reset(); |
|
|
|
|
m->videoProgress("stopped"); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop video worker stopped"); |
|
|
|
|
#else |
|
|
|
|
Q_UNUSED(initial); |
|
|
|
|
#endif |
|
|
|
|
@ -767,11 +805,15 @@ void StreamPublishEngine::tick() {
|
|
|
|
|
if (!confirmed && !waitingSelection && !softwareEncodingRequest() && monotonicUs() - started > 8000000) |
|
|
|
|
sourceFailed(m->generation, text("Timed out waiting for the first video frame")); |
|
|
|
|
{ std::lock_guard<std::mutex> guard(m->lock); |
|
|
|
|
if (monotonicUs() - m->captureReport >= 1000000) { |
|
|
|
|
if (monotonicUs() - m->captureReport >= 5000000) { |
|
|
|
|
m->captureReport = monotonicUs(); |
|
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop capture state source_generation=%llu video_generation=%llu confirmed=%llu raw_frames=%llu raw_PTS=%lld", |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop capture progress source_generation=%llu video_generation=%llu confirmed=%llu raw_frames=%llu raw_PTS=%lld " |
|
|
|
|
"converted=%llu encoded=%llu delivered=%llu repeated=%llu stage=%s stage_age_ms=%lld", |
|
|
|
|
(unsigned long long)m->sourceGeneration, (unsigned long long)m->generation, (unsigned long long)m->confirmedGeneration, |
|
|
|
|
(unsigned long long)m->capturedFrames, (long long)m->framePts); |
|
|
|
|
(unsigned long long)m->capturedFrames, (long long)m->framePts, |
|
|
|
|
(unsigned long long)m->convertedFrames.load(), (unsigned long long)m->encodedFrames.load(), |
|
|
|
|
(unsigned long long)m->deliveredFrames.load(), (unsigned long long)m->repeatedFrames.load(), |
|
|
|
|
m->videoStage.load(), (long long)((monotonicUs() - m->videoStageAt.load()) / 1000)); |
|
|
|
|
} } |
|
|
|
|
} |
|
|
|
|
unsigned dropped = m->audioDropped.exchange(0); |
|
|
|
|
|