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