[fix] prevent system audio capture hangs across desktop platforms

This commit is contained in:
dijunkun
2026-09-09 11:34:47 +08:00
parent b87ab025be
commit bd0a7aa2f6
16 changed files with 743 additions and 575 deletions
@@ -0,0 +1,165 @@
#include "speaker_capture_controller.h"
#include <algorithm>
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <mutex>
#include <thread>
#include <utility>
#include "rd_log.h"
namespace crossdesk {
struct SpeakerCaptureController::State {
Factory factory;
RetryPolicy policy;
std::mutex mutex;
std::condition_variable wake;
bool enabled = false;
bool shutdown = false;
uint64_t generation = 0;
std::atomic<bool> running{false};
// Never hold this mutex across a native capture operation. It only drains
// an in-flight audio delivery when a session is disabled/destroyed.
std::mutex callback_mutex;
SpeakerCapturer::speaker_data_cb callback;
bool deliver = false;
uint64_t callback_attempt = 0;
};
SpeakerCaptureController::SpeakerCaptureController(
Factory factory, SpeakerCapturer::speaker_data_cb cb)
: SpeakerCaptureController(std::move(factory), std::move(cb),
RetryPolicy{}) {}
SpeakerCaptureController::SpeakerCaptureController(
Factory factory, SpeakerCapturer::speaker_data_cb cb, RetryPolicy policy)
: state_(std::make_shared<State>()) {
state_->factory = std::move(factory);
state_->callback = std::move(cb);
policy.max_attempts = std::max(1u, policy.max_attempts);
state_->policy = policy;
// The worker owns only shared state, never this/controller/GUI. Even a
// driver stuck inside a native API cannot block shutdown or outlive an
// unguarded callback target. There is at most one worker per controller.
std::thread([state = state_] { Run(state); }).detach();
}
SpeakerCaptureController::~SpeakerCaptureController() { Shutdown(); }
void SpeakerCaptureController::SetEnabled(bool enabled) {
std::lock_guard<std::mutex> lock(state_->mutex);
if (state_->shutdown || state_->enabled == enabled) return;
state_->enabled = enabled;
++state_->generation;
if (!enabled) {
std::lock_guard<std::mutex> callback_lock(state_->callback_mutex);
state_->deliver = false;
state_->running = false;
}
state_->wake.notify_one();
}
bool SpeakerCaptureController::IsRunning() const { return state_->running; }
void SpeakerCaptureController::Shutdown() {
std::lock_guard<std::mutex> lock(state_->mutex);
state_->shutdown = true;
state_->enabled = false;
state_->running = false;
{
std::lock_guard<std::mutex> callback_lock(state_->callback_mutex);
state_->deliver = false;
state_->callback = nullptr;
}
state_->wake.notify_one();
}
void SpeakerCaptureController::Run(const std::shared_ptr<State>& state) {
uint64_t callback_attempt = 0;
std::unique_lock<std::mutex> lock(state->mutex);
while (!state->shutdown) {
state->wake.wait(lock, [&] { return state->shutdown || state->enabled; });
if (state->shutdown) break;
const uint64_t generation = state->generation;
const auto cancelled = [&] {
return state->shutdown || !state->enabled ||
state->generation != generation;
};
for (unsigned attempt = 1; !cancelled(); ++attempt) {
const uint64_t current_attempt = ++callback_attempt;
lock.unlock();
auto capturer = state->factory();
std::weak_ptr<State> weak = state;
auto callback = [weak, current_attempt](unsigned char* data, size_t size,
const char* name) {
if (auto current = weak.lock()) {
std::lock_guard<std::mutex> callback_lock(current->callback_mutex);
if (current->deliver &&
current->callback_attempt == current_attempt &&
current->callback) {
current->callback(data, size, name);
}
}
};
const bool initialized = capturer && capturer->Init(callback) == 0;
lock.lock();
const bool should_start = initialized && !cancelled();
lock.unlock();
const bool started = should_start && capturer->Start() == 0;
lock.lock();
if (started && !cancelled()) {
{
std::lock_guard<std::mutex> callback_lock(state->callback_mutex);
state->callback_attempt = current_attempt;
state->deliver = true;
state->running = true;
}
LOG_INFO("System audio capture started (attempt {})", attempt);
while (!cancelled()) {
if (state->wake.wait_for(lock, state->policy.health_interval,
cancelled))
break;
lock.unlock();
const bool healthy = capturer->IsRunning();
lock.lock();
if (!healthy) {
LOG_WARN("System audio capture stopped unexpectedly");
break;
}
}
}
{
std::lock_guard<std::mutex> callback_lock(state->callback_mutex);
state->deliver = false;
state->running = false;
}
lock.unlock();
// Recreate on every attempt/reconnection to discard stale displays,
// PulseAudio sources and invalidated WASAPI devices.
if (capturer) capturer->Destroy();
capturer.reset();
lock.lock();
if (cancelled()) break;
if (attempt >= state->policy.max_attempts) {
LOG_ERROR(
"System audio capture disabled after {} attempts; toggle audio "
"or reconnect to retry",
attempt);
state->wake.wait(lock, cancelled);
break;
}
const auto delay =
state->policy.initial_delay * (1u << std::min(attempt - 1, 5u));
LOG_WARN("System audio capture failed (attempt {}); retry in {} ms",
attempt, delay.count());
state->wake.wait_for(lock, delay, cancelled);
}
}
}
} // namespace crossdesk
@@ -0,0 +1,50 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-09
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SPEAKER_CAPTURE_CONTROLLER_H_
#define _SPEAKER_CAPTURE_CONTROLLER_H_
#include <chrono>
#include <functional>
#include <memory>
#include "speaker_capturer.h"
namespace crossdesk {
// Serializes platform capture operations away from the UI and transport
// threads. Stop/Shutdown revoke callbacks before returning; native teardown
// can then finish without keeping the GUI or its peer alive.
class SpeakerCaptureController {
public:
using Factory = std::function<std::unique_ptr<SpeakerCapturer>()>;
struct RetryPolicy {
unsigned max_attempts = 3;
std::chrono::milliseconds initial_delay{1000};
std::chrono::milliseconds health_interval{100};
};
SpeakerCaptureController(Factory factory,
SpeakerCapturer::speaker_data_cb cb);
SpeakerCaptureController(Factory factory, SpeakerCapturer::speaker_data_cb cb,
RetryPolicy policy);
~SpeakerCaptureController();
SpeakerCaptureController(const SpeakerCaptureController&) = delete;
SpeakerCaptureController& operator=(const SpeakerCaptureController&) = delete;
void SetEnabled(bool enabled);
bool IsRunning() const;
void Shutdown();
private:
struct State;
static void Run(const std::shared_ptr<State>& state);
std::shared_ptr<State> state_;
};
} // namespace crossdesk
#endif
@@ -24,6 +24,8 @@ class SpeakerCapturer {
virtual int Destroy() = 0;
virtual int Start() = 0;
virtual int Stop() = 0;
// Called on the same control worker as Init/Start/Stop. Must not block.
virtual bool IsRunning() const = 0;
};
} // namespace crossdesk
#endif
#endif