mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-20 19:14:39 +08:00
[fix] prevent system audio capture hangs across desktop platforms
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user