Browse Source

Report capture termination and preserve explicit microphone selection

master
evgeny 5 days ago
parent
commit
2caa9504c9
  1. 1
      tools/chatgui/src/messagelist.cpp
  2. 38
      tools/chatgui/src/stream_publish_engine.cpp
  3. 5
      tools/chatgui/transport/stream_bridge.h

1
tools/chatgui/src/messagelist.cpp

@ -35,6 +35,7 @@
#include <QFileDialog> #include <QFileDialog>
#include <QToolTip> #include <QToolTip>
#include <vector> #include <vector>
#include <algorithm>
#include <memory> #include <memory>
#include <thread> #include <thread>

38
tools/chatgui/src/stream_publish_engine.cpp

@ -40,7 +40,7 @@ struct StreamPublishEngine::Impl {
std::condition_variable wake; std::condition_variable wake;
std::thread videoThread, audioThread, probeThread; std::thread videoThread, audioThread, probeThread;
std::atomic<bool> running{false}, muted{false}; std::atomic<bool> running{false}, muted{false};
std::atomic<unsigned> audioDropped{0}; std::atomic<unsigned> audioDropped{0}, audioMissing{0};
AudioEventQueue<Audio, 32> audio; AudioEventQueue<Audio, 32> audio;
uint64_t id = 0, group = 0, generation = 0, sourceGeneration = 0, confirmedGeneration = 0, frameGeneration = 0, audioSequence = 0; uint64_t id = 0, group = 0, generation = 0, sourceGeneration = 0, confirmedGeneration = 0, frameGeneration = 0, audioSequence = 0;
int64_t origin = 0, started = 0, framePts = -1, backendOrigin = -1, backendAnchor = -1; int64_t origin = 0, started = 0, framePts = -1, backendOrigin = -1, backendAnchor = -1;
@ -175,9 +175,10 @@ bool StreamPublishEngine::openSource(const StreamPublishConfig& config) {
} }
if (best.isNull()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop camera lacks requested fps=%d", config.video.fps); return false; } if (best.isNull()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop camera lacks requested fps=%d", config.video.fps); return false; }
m->camera->setCameraFormat(best); m->session->setCamera(m->camera.get()); m->camera->setCameraFormat(best); m->session->setCamera(m->camera.get());
connect(m->camera.get(), &QCamera::activeChanged, this, [generation](bool active) { connect(m->camera.get(), &QCamera::activeChanged, this, [this, generation](bool active) {
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop camera active=%d generation=%llu", active, (unsigned long long)generation); DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop camera active=%d generation=%llu", active, (unsigned long long)generation);
}); if (!active && generation == m->sourceGeneration) sourceFailed(m->generation, text("Video capture stopped"));
}, Qt::QueuedConnection);
connect(m->camera.get(), &QCamera::errorOccurred, this, [failed](QCamera::Error, const QString& error) { failed(error); }, Qt::QueuedConnection); connect(m->camera.get(), &QCamera::errorOccurred, this, [failed](QCamera::Error, const QString& error) { failed(error); }, Qt::QueuedConnection);
m->camera->start(); m->camera->start();
} else if (config.source.kind == StreamVideoSource::Screen) { } else if (config.source.kind == StreamVideoSource::Screen) {
@ -189,18 +190,20 @@ bool StreamPublishEngine::openSource(const StreamPublishConfig& config) {
m->screen->setScreen(selected); m->screen->setScreen(selected);
} }
m->session->setScreenCapture(m->screen.get()); m->session->setScreenCapture(m->screen.get());
connect(m->screen.get(), &QScreenCapture::activeChanged, this, [generation](bool active) { connect(m->screen.get(), &QScreenCapture::activeChanged, this, [this, generation](bool active) {
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop screen capture active=%d generation=%llu", active, (unsigned long long)generation); DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop screen capture active=%d generation=%llu", active, (unsigned long long)generation);
}); if (!active && generation == m->sourceGeneration) sourceFailed(m->generation, text("Video capture stopped"));
}, Qt::QueuedConnection);
connect(m->screen.get(), &QScreenCapture::errorOccurred, this, [failed](QScreenCapture::Error, const QString& error) { failed(error); }, Qt::QueuedConnection); connect(m->screen.get(), &QScreenCapture::errorOccurred, this, [failed](QScreenCapture::Error, const QString& error) { failed(error); }, Qt::QueuedConnection);
m->screen->start(); m->screen->start();
} else { } else {
auto window = config.source.window.value<QCapturableWindow>(); auto window = config.source.window.value<QCapturableWindow>();
if (!window.isValid()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop window is unavailable"); return false; } if (!window.isValid()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop window is unavailable"); return false; }
m->window = std::make_unique<QWindowCapture>(); m->window->setWindow(window); m->session->setWindowCapture(m->window.get()); m->window = std::make_unique<QWindowCapture>(); m->window->setWindow(window); m->session->setWindowCapture(m->window.get());
connect(m->window.get(), &QWindowCapture::activeChanged, this, [generation](bool active) { connect(m->window.get(), &QWindowCapture::activeChanged, this, [this, generation](bool active) {
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop window capture active=%d generation=%llu", active, (unsigned long long)generation); DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop window capture active=%d generation=%llu", active, (unsigned long long)generation);
}); if (!active && generation == m->sourceGeneration) sourceFailed(m->generation, text("Video capture stopped"));
}, Qt::QueuedConnection);
connect(m->window.get(), &QWindowCapture::errorOccurred, this, [failed](QWindowCapture::Error, const QString& error) { failed(error); }, Qt::QueuedConnection); connect(m->window.get(), &QWindowCapture::errorOccurred, this, [failed](QWindowCapture::Error, const QString& error) { failed(error); }, Qt::QueuedConnection);
m->window->start(); m->window->start();
} }
@ -224,8 +227,11 @@ void StreamPublishEngine::closeSource() {
} }
bool StreamPublishEngine::start(uint64_t group, const StreamPublishConfig& config) { bool StreamPublishEngine::start(uint64_t group, const StreamPublishConfig& config) {
if (active()) { emit errorOccurred(text("A stream is already running")); return false; } if (active()) { DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop publication already active"); emit errorOccurred(text("A stream is already running")); return false; }
if (!supported() || !codecs().contains(config.video.codec)) { emit errorOccurred(text("Video capture or encoder is unavailable")); return false; } if (!supported() || !codecs().contains(config.video.codec)) {
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.audio && !m_microphone.acquire()) { if (config.audio && !m_microphone.acquire()) {
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop publication blocked: microphone owned by another operation"); 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; emit errorOccurred(text("The microphone is used by another operation")); return false;
@ -248,7 +254,7 @@ bool StreamPublishEngine::start(uint64_t group, const StreamPublishConfig& confi
if (!openSource(config) || (config.audio && !openMicrophone())) { if (!openSource(config) || (config.audio && !openMicrophone())) {
sourceFailed(m->generation, text("Could not open the selected capture device")); return false; sourceFailed(m->generation, text("Could not open the selected capture device")); return false;
} }
emit stateChanged(config.source.id == "portal" ? text("Select a screen or window in the system dialog") : text("Starting stream…")); emit stateChanged(config.source.id == "portal" ? text("Select a screen in the system dialog") : text("Starting stream…"));
emit configurationChanged(); return true; emit configurationChanged(); return true;
} }
@ -256,6 +262,7 @@ bool StreamPublishEngine::reconfigure(const StreamPublishConfig& config) {
if (!m->running) return false; if (!m->running) return false;
StreamPublishConfig previous = this->config(); StreamPublishConfig previous = this->config();
if (config.audio != previous.audio || config.microphone != previous.microphone) { if (config.audio != previous.audio || config.microphone != previous.microphone) {
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop microphone selection changed during live publication");
emit errorOccurred(text("Change the microphone before starting the stream")); return false; emit errorOccurred(text("Change the microphone before starting the stream")); return false;
} }
bool sourceChange = config.source.kind != previous.source.kind || config.source.id != previous.source.id bool sourceChange = config.source.kind != previous.source.kind || config.source.id != previous.source.id
@ -298,6 +305,7 @@ bool StreamPublishEngine::openMicrophone() {
auto config = this->config(); auto* context = SoundManager::instance()->context(); auto config = this->config(); auto* context = SoundManager::instance()->context();
if (!context) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop stream audio context unavailable"); return false; } if (!context) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop stream audio context unavailable"); return false; }
ma_device_id device; bool selected = SoundManager::deviceId(config.microphone, &device); ma_device_id device; bool selected = SoundManager::deviceId(config.microphone, &device);
if (!config.microphone.isEmpty() && !selected) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop selected microphone ID is unavailable"); return false; }
for (int channels : {2, 1}) { for (int channels : {2, 1}) {
ma_device_config capture = ma_device_config_init(ma_device_type_capture); ma_device_config capture = ma_device_config_init(ma_device_type_capture);
capture.capture.format = ma_format_s16; capture.capture.channels = channels; capture.sampleRate = 48000; capture.capture.format = ma_format_s16; capture.capture.channels = channels; capture.sampleRate = 48000;
@ -323,7 +331,7 @@ void StreamPublishEngine::captureAudio(const void* input, unsigned frames) {
m->audioAnchor = first; m->audioFrames = 0; m->audioAnchor = first; m->audioFrames = 0;
} }
first = m->audioAnchor + (int64_t)m->audioFrames * 1000000 / 48000; m->audioFrames += frames; first = m->audioAnchor + (int64_t)m->audioFrames * 1000000 / 48000; m->audioFrames += frames;
if (!pcm) m->audioDropped++; if (!pcm) m->audioMissing++;
for (unsigned offset = 0; offset < frames;) { for (unsigned offset = 0; offset < frames;) {
Impl::Audio record; record.frames = std::min(960u, frames - offset); Impl::Audio record; record.frames = std::min(960u, frames - offset);
record.pts = std::max<int64_t>(0, first + (int64_t)offset * 1000000 / 48000); record.created = monotonicUs(); record.pts = std::max<int64_t>(0, first + (int64_t)offset * 1000000 / 48000); record.created = monotonicUs();
@ -424,7 +432,11 @@ void StreamPublishEngine::videoLoop() {
if (frame.isValid()) { if (frame.isValid()) {
if (capture <= lastPts || monotonicUs() - m->origin - capture > 250000) { skipped++; continue; } if (capture <= lastPts || monotonicUs() - m->origin - capture > 250000) { skipped++; continue; }
lastImage = frame.toImage(); lastImage = frame.toImage();
if (lastImage.isNull()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop captured frame conversion failed"); continue; } 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")); });
continue;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
if (frame.rotation() != QtVideo::Rotation::None) lastImage = lastImage.transformed(QTransform().rotate(static_cast<int>(frame.rotation()))); if (frame.rotation() != QtVideo::Rotation::None) lastImage = lastImage.transformed(QTransform().rotate(static_cast<int>(frame.rotation())));
if (frame.mirrored()) lastImage = lastImage.transformed(QTransform().scale(-1, 1)); if (frame.mirrored()) lastImage = lastImage.transformed(QTransform().scale(-1, 1));
@ -508,4 +520,6 @@ void StreamPublishEngine::tick() {
} }
unsigned dropped = m->audioDropped.exchange(0); unsigned dropped = m->audioDropped.exchange(0);
if (dropped) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop audio capture discarded blocks=%u", dropped); if (dropped) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop audio capture discarded blocks=%u", dropped);
unsigned missing = m->audioMissing.exchange(0);
if (missing) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop microphone callbacks lack PCM count=%u; sent silence", missing);
} }

5
tools/chatgui/transport/stream_bridge.h

@ -1,5 +1,6 @@
/* Desktop live viewer: attach/detach в uasync до destroy ядра; остальные вызовы потокобезопасны. /* Desktop live media: attach/detach в uasync до destroy ядра; остальные вызовы потокобезопасны.
* Один viewer, команды содержат IDs; подготовлено не более двух media records, общий предел RX 4MiB/16 записей. * Один общий publisher и один viewer; команды содержат IDs, не borrowed pointers ядра.
* Подготовлено не более двух media records, общий предел RX 4MiB/16 записей.
* Watch до attach запоминает выбранную группу. Каталог идёт через CHAT_EVT_STREAM_SOURCES, * Watch до attach запоминает выбранную группу. Каталог идёт через CHAT_EVT_STREAM_SOURCES,
* media pull читает ближайшие кадры из общего RS-кольца; копия нужна только для передачи между потоками. */ * media pull читает ближайшие кадры из общего RS-кольца; копия нужна только для передачи между потоками. */
#pragma once #pragma once

Loading…
Cancel
Save