mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-09-01 06:12:01 +08:00
585 lines
18 KiB
C++
585 lines
18 KiB
C++
#include "features/devices/session_device_manager.h"
|
|
|
|
#include <remote_action.h>
|
|
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
|
|
#include <display_stream_id.h>
|
|
#include "platform.h"
|
|
#include "rd_log.h"
|
|
#include "runtime/gui_runtime.h"
|
|
|
|
namespace crossdesk {
|
|
namespace {
|
|
|
|
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
|
|
constexpr size_t kMaxCapturedKeyboardInputs = 512;
|
|
constexpr auto kFrameDeadlineTolerance = std::chrono::milliseconds(1);
|
|
|
|
} // namespace
|
|
|
|
SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner) : owner_(owner) {}
|
|
|
|
bool SessionDeviceManager::ShouldSendCapturedFrame(
|
|
std::chrono::steady_clock::time_point now, int fps) {
|
|
const auto interval =
|
|
std::chrono::nanoseconds(std::chrono::seconds(1)) / (fps > 0 ? fps : 1);
|
|
if (next_frame_deadline_ == std::chrono::steady_clock::time_point{}) {
|
|
next_frame_deadline_ = now;
|
|
}
|
|
if (now + kFrameDeadlineTolerance < next_frame_deadline_) {
|
|
return false;
|
|
}
|
|
next_frame_deadline_ += interval;
|
|
if (next_frame_deadline_ <= now) {
|
|
next_frame_deadline_ = now + interval;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void SessionDeviceManager::Initialize() {
|
|
InitializeAudioOutput();
|
|
screen_capturer_factory_ = new ScreenCapturerFactory();
|
|
speaker_capturer_factory_ = new SpeakerCapturerFactory();
|
|
device_controller_factory_ = new DeviceControllerFactory();
|
|
keyboard_capturer_ =
|
|
static_cast<KeyboardCapturer *>(device_controller_factory_->Create(
|
|
DeviceControllerFactory::Device::Keyboard));
|
|
}
|
|
|
|
int SessionDeviceManager::InitializeScreenCapturer() {
|
|
#ifdef __APPLE__
|
|
if (!owner_.EnsureMacScreenRecordingPermission()) {
|
|
return -1;
|
|
}
|
|
#endif
|
|
|
|
if (!screen_capturer_) {
|
|
screen_capturer_ =
|
|
static_cast<ScreenCapturer *>(screen_capturer_factory_->Create());
|
|
}
|
|
|
|
last_frame_time_ = {};
|
|
next_frame_deadline_ = {};
|
|
const int fps = owner_.config_center_->GetVideoFrameRate() ==
|
|
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
|
? 30
|
|
: 60;
|
|
LOG_INFO("Init screen capturer with {} fps", fps);
|
|
display_info_list_.clear();
|
|
registered_display_stream_count_ = 0;
|
|
last_video_frame_stream_id_.clear();
|
|
invalid_video_stream_id_logged_ = false;
|
|
|
|
const int init_ret = screen_capturer_->Init(
|
|
fps, [this, fps](unsigned char *data, int size, int width, int height,
|
|
const char *display_name) {
|
|
const auto now_time = std::chrono::steady_clock::now();
|
|
if (!ShouldSendCapturedFrame(now_time, fps)) {
|
|
return;
|
|
}
|
|
|
|
const bool has_previous_frame =
|
|
last_frame_time_.time_since_epoch().count() != 0;
|
|
const auto duration_ms =
|
|
has_previous_frame
|
|
? std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
now_time - last_frame_time_)
|
|
.count()
|
|
: 0;
|
|
|
|
std::vector<std::string> connected_remote_ids;
|
|
{
|
|
std::shared_lock lock(owner_.connection_status_mutex_);
|
|
connected_remote_ids.reserve(owner_.connection_status_.size());
|
|
for (const auto &[remote_id, status] : owner_.connection_status_) {
|
|
if (status == ConnectionStatus::Connected) {
|
|
connected_remote_ids.push_back(remote_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Capture can still deliver frames while ICE is gathering or after
|
|
// the final controller disconnects. Do not broadcast those frames to
|
|
// MiniRTC: a broadcast also reaches newly joining peers whose ICE
|
|
// transport is not ready yet.
|
|
if (connected_remote_ids.empty()) {
|
|
last_frame_time_ = now_time;
|
|
return;
|
|
}
|
|
|
|
const std::string stream_id = ResolveDisplayStreamId(
|
|
display_name, registered_display_stream_count_, -1,
|
|
last_video_frame_stream_id_);
|
|
if (stream_id.empty()) {
|
|
if (!invalid_video_stream_id_logged_) {
|
|
LOG_ERROR(
|
|
"Drop captured frames with an empty or unregistered video "
|
|
"stream id, reported='{}', registered_streams={}",
|
|
display_name ? display_name : "",
|
|
registered_display_stream_count_);
|
|
invalid_video_stream_id_logged_ = true;
|
|
}
|
|
return;
|
|
}
|
|
invalid_video_stream_id_logged_ = false;
|
|
const bool resumed_after_gap =
|
|
has_previous_frame && duration_ms >= kCaptureResumeKeyFrameGapMs;
|
|
const bool stream_changed = !last_video_frame_stream_id_.empty() &&
|
|
last_video_frame_stream_id_ != stream_id;
|
|
if (resumed_after_gap || stream_changed) {
|
|
if (RequestVideoKeyFrame(owner_.peer_, stream_id.c_str()) == 0) {
|
|
LOG_INFO("Request video key frame before sending captured frame, "
|
|
"stream='{}', gap_ms={}, stream_changed={}",
|
|
stream_id, duration_ms, stream_changed);
|
|
}
|
|
}
|
|
|
|
XVideoFrame frame{};
|
|
frame.data = reinterpret_cast<const char *>(data);
|
|
frame.size = size;
|
|
frame.width = width;
|
|
frame.height = height;
|
|
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
|
for (const std::string &remote_id : connected_remote_ids) {
|
|
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
|
|
remote_id.data(), remote_id.size());
|
|
}
|
|
last_video_frame_stream_id_ = stream_id;
|
|
last_frame_time_ = now_time;
|
|
});
|
|
|
|
if (init_ret == 0) {
|
|
LOG_INFO("Init screen capturer success");
|
|
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
|
|
if (!latest_display_info.empty()) {
|
|
display_info_list_ = latest_display_info;
|
|
}
|
|
registered_display_stream_count_ = display_info_list_.size();
|
|
return 0;
|
|
}
|
|
|
|
LOG_ERROR("Init screen capturer failed");
|
|
screen_capturer_->Destroy();
|
|
delete screen_capturer_;
|
|
screen_capturer_ = nullptr;
|
|
return -1;
|
|
}
|
|
|
|
int SessionDeviceManager::StartScreenCapturer() {
|
|
#ifdef __APPLE__
|
|
if (!owner_.EnsureMacScreenRecordingPermission()) {
|
|
return -1;
|
|
}
|
|
#endif
|
|
|
|
if (!screen_capturer_) {
|
|
LOG_INFO("Screen capturer instance missing, recreating before start");
|
|
if (InitializeScreenCapturer() != 0) {
|
|
LOG_ERROR("Recreate screen capturer failed");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
LOG_INFO("Start screen capturer, show cursor: {}", owner_.show_cursor_);
|
|
const int ret = screen_capturer_->Start(owner_.show_cursor_);
|
|
if (ret != 0) {
|
|
LOG_ERROR("Start screen capturer failed: {}", ret);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
int SessionDeviceManager::StopScreenCapturer() {
|
|
if (screen_capturer_) {
|
|
LOG_INFO("Stop screen capturer");
|
|
screen_capturer_->Stop();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int SessionDeviceManager::StartSpeakerCapturer() {
|
|
if (!speaker_capturer_) {
|
|
speaker_capturer_ =
|
|
static_cast<SpeakerCapturer *>(speaker_capturer_factory_->Create());
|
|
const int init_ret = speaker_capturer_->Init(
|
|
[this](unsigned char *data, size_t size, const char *audio_name) {
|
|
SendAudioFrame(owner_.peer_, reinterpret_cast<const char *>(data),
|
|
size, owner_.audio_label_.c_str());
|
|
});
|
|
|
|
if (init_ret != 0) {
|
|
speaker_capturer_->Destroy();
|
|
delete speaker_capturer_;
|
|
speaker_capturer_ = nullptr;
|
|
}
|
|
}
|
|
|
|
if (!speaker_capturer_) {
|
|
return -1;
|
|
}
|
|
|
|
const int ret = speaker_capturer_->Start();
|
|
if (ret != 0) {
|
|
LOG_ERROR("Start speaker capturer failed: {}", ret);
|
|
return ret;
|
|
}
|
|
owner_.start_speaker_capturer_ = true;
|
|
return 0;
|
|
}
|
|
|
|
int SessionDeviceManager::StopSpeakerCapturer() {
|
|
if (speaker_capturer_) {
|
|
speaker_capturer_->Stop();
|
|
owner_.start_speaker_capturer_ = false;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int SessionDeviceManager::StartMouseController() {
|
|
#ifdef __APPLE__
|
|
if (!owner_.EnsureMacAccessibilityPermission()) {
|
|
return -1;
|
|
}
|
|
#endif
|
|
|
|
if (!device_controller_factory_) {
|
|
LOG_INFO("Device controller factory is nullptr");
|
|
return -1;
|
|
}
|
|
|
|
#if defined(__linux__) && !defined(__APPLE__)
|
|
if (IsWaylandSession()) {
|
|
if (!screen_capturer_) {
|
|
return 1;
|
|
}
|
|
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
|
|
if (latest_display_info.empty() ||
|
|
latest_display_info[0].handle == nullptr) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (screen_capturer_) {
|
|
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
|
|
if (!latest_display_info.empty()) {
|
|
display_info_list_ = latest_display_info;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
mouse_controller_ =
|
|
static_cast<MouseController *>(device_controller_factory_->Create(
|
|
DeviceControllerFactory::Device::Mouse));
|
|
if (!mouse_controller_) {
|
|
LOG_ERROR("Create mouse controller failed");
|
|
return -1;
|
|
}
|
|
|
|
const int init_ret = mouse_controller_->Init(display_info_list_);
|
|
if (init_ret != 0) {
|
|
LOG_INFO("Destroy mouse controller");
|
|
mouse_controller_->Destroy();
|
|
delete mouse_controller_;
|
|
mouse_controller_ = nullptr;
|
|
}
|
|
return init_ret;
|
|
}
|
|
|
|
int SessionDeviceManager::StopMouseController() {
|
|
if (mouse_controller_) {
|
|
mouse_controller_->Destroy();
|
|
delete mouse_controller_;
|
|
mouse_controller_ = nullptr;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int SessionDeviceManager::StartKeyboardCapturer() {
|
|
owner_.keyboard_capturer_uses_window_events_ = false;
|
|
|
|
#ifdef __APPLE__
|
|
if (!owner_.EnsureMacAccessibilityPermission()) {
|
|
owner_.keyboard_capturer_uses_window_events_ = true;
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
#if defined(__linux__) && !defined(__APPLE__)
|
|
if (IsWaylandSession()) {
|
|
owner_.keyboard_capturer_uses_window_events_ = true;
|
|
LOG_INFO("Start keyboard capturer with Slint Wayland backend");
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
if (!keyboard_capturer_) {
|
|
owner_.keyboard_capturer_uses_window_events_ = true;
|
|
LOG_WARN(
|
|
"keyboard capturer is nullptr, falling back to Slint keyboard events");
|
|
return 0;
|
|
}
|
|
|
|
const int hook_ret = keyboard_capturer_->Hook(
|
|
[](int key_code, bool is_down, uint32_t scan_code, bool extended,
|
|
void *user_ptr) {
|
|
if (user_ptr) {
|
|
auto *devices = static_cast<SessionDeviceManager *>(user_ptr);
|
|
devices->QueueCapturedKeyboardInput(key_code, is_down, scan_code,
|
|
extended);
|
|
}
|
|
},
|
|
this);
|
|
if (hook_ret != 0) {
|
|
owner_.keyboard_capturer_uses_window_events_ = true;
|
|
LOG_WARN(
|
|
"Start keyboard capturer failed, falling back to Slint keyboard events");
|
|
} else {
|
|
LOG_INFO("Start keyboard capturer with native input");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int SessionDeviceManager::StopKeyboardCapturer() {
|
|
if (owner_.keyboard_capturer_uses_window_events_) {
|
|
owner_.keyboard_capturer_uses_window_events_ = false;
|
|
LOG_INFO("Stop keyboard capturer with Slint keyboard backend");
|
|
return 0;
|
|
}
|
|
|
|
if (keyboard_capturer_) {
|
|
keyboard_capturer_->Unhook();
|
|
ClearCapturedKeyboardInput();
|
|
LOG_INFO("Stop keyboard capturer");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int SessionDeviceManager::InitializeAudioOutput() {
|
|
SDL_AudioSpec desired_out{};
|
|
desired_out.freq = 48000;
|
|
desired_out.format = SDL_AUDIO_S16;
|
|
desired_out.channels = 1;
|
|
|
|
auto open_stream = [&]() {
|
|
output_stream_ = SDL_OpenAudioDeviceStream(
|
|
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_out, nullptr, nullptr);
|
|
return output_stream_ != nullptr;
|
|
};
|
|
|
|
if (!open_stream()) {
|
|
#if defined(__linux__) && !defined(__APPLE__)
|
|
LOG_WARN("Failed to open output stream with driver [{}]: {}",
|
|
getenv("SDL_AUDIODRIVER") ? getenv("SDL_AUDIODRIVER")
|
|
: "(default)",
|
|
SDL_GetError());
|
|
|
|
setenv("SDL_AUDIODRIVER", "dummy", 1);
|
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
|
|
LOG_ERROR("Failed to reinitialize SDL audio with dummy driver: {}",
|
|
SDL_GetError());
|
|
return -1;
|
|
}
|
|
if (!open_stream()) {
|
|
LOG_ERROR("Failed to open output stream with dummy driver: {}",
|
|
SDL_GetError());
|
|
return -1;
|
|
}
|
|
LOG_WARN("Audio output disabled, using SDL dummy audio driver");
|
|
#else
|
|
LOG_ERROR("Failed to open output stream: {}", SDL_GetError());
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(output_stream_));
|
|
return 0;
|
|
}
|
|
|
|
int SessionDeviceManager::DestroyAudioOutput() {
|
|
if (output_stream_) {
|
|
SDL_CloseAudioDevice(SDL_GetAudioStreamDevice(output_stream_));
|
|
SDL_DestroyAudioStream(output_stream_);
|
|
output_stream_ = nullptr;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void SessionDeviceManager::PushAudio(const char *data, size_t size) {
|
|
if (!output_stream_) {
|
|
return;
|
|
}
|
|
const int pushed = SDL_PutAudioStreamData(
|
|
output_stream_, reinterpret_cast<const Uint8 *>(data),
|
|
static_cast<int>(size));
|
|
if (pushed < 0) {
|
|
LOG_ERROR("Failed to push audio data: {}", SDL_GetError());
|
|
}
|
|
}
|
|
|
|
void SessionDeviceManager::UpdateInteractions() {
|
|
#if defined(__linux__) && !defined(__APPLE__)
|
|
const bool is_wayland_session = IsWaylandSession();
|
|
const bool stop_wayland_mouse_before_screen =
|
|
is_wayland_session && !owner_.start_screen_capturer_ &&
|
|
owner_.screen_capturer_is_started_ && !owner_.start_mouse_controller_ &&
|
|
owner_.mouse_controller_is_started_;
|
|
if (stop_wayland_mouse_before_screen) {
|
|
LOG_INFO("Stopping Wayland mouse controller before screen capturer to "
|
|
"cleanly release the shared portal session");
|
|
StopMouseController();
|
|
owner_.mouse_controller_is_started_ = false;
|
|
}
|
|
#endif
|
|
|
|
if (owner_.start_screen_capturer_ && !owner_.screen_capturer_is_started_) {
|
|
if (StartScreenCapturer() == 0) {
|
|
owner_.screen_capturer_is_started_ = true;
|
|
}
|
|
} else if (!owner_.start_screen_capturer_ &&
|
|
owner_.screen_capturer_is_started_) {
|
|
StopScreenCapturer();
|
|
owner_.screen_capturer_is_started_ = false;
|
|
}
|
|
|
|
if (owner_.start_speaker_capturer_ && !owner_.speaker_capturer_is_started_) {
|
|
if (StartSpeakerCapturer() == 0) {
|
|
owner_.speaker_capturer_is_started_ = true;
|
|
}
|
|
} else if (!owner_.start_speaker_capturer_ &&
|
|
owner_.speaker_capturer_is_started_) {
|
|
StopSpeakerCapturer();
|
|
owner_.speaker_capturer_is_started_ = false;
|
|
}
|
|
|
|
if (owner_.start_mouse_controller_ && !owner_.mouse_controller_is_started_) {
|
|
if (StartMouseController() == 0) {
|
|
owner_.mouse_controller_is_started_ = true;
|
|
}
|
|
} else if (!owner_.start_mouse_controller_ &&
|
|
owner_.mouse_controller_is_started_) {
|
|
StopMouseController();
|
|
owner_.mouse_controller_is_started_ = false;
|
|
}
|
|
|
|
#if defined(__linux__) || defined(__APPLE__)
|
|
if (owner_.screen_capturer_is_started_ && screen_capturer_ &&
|
|
mouse_controller_) {
|
|
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
|
|
if (!latest_display_info.empty()) {
|
|
display_info_list_ = latest_display_info;
|
|
mouse_controller_->UpdateDisplayInfoList(display_info_list_);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_) {
|
|
if (!owner_.keyboard_capturer_is_started_ && StartKeyboardCapturer() == 0) {
|
|
owner_.keyboard_capturer_is_started_ = true;
|
|
}
|
|
if (owner_.keyboard_capturer_is_started_) {
|
|
DrainCapturedKeyboardInput();
|
|
owner_.keyboard_.SendHeartbeat(false);
|
|
}
|
|
} else if (owner_.keyboard_capturer_is_started_) {
|
|
owner_.keyboard_.ForceReleasePressedKeys();
|
|
StopKeyboardCapturer();
|
|
owner_.keyboard_capturer_is_started_ = false;
|
|
}
|
|
|
|
owner_.keyboard_.CheckRemoteTimeouts();
|
|
}
|
|
|
|
void SessionDeviceManager::QueueCapturedKeyboardInput(int key_code,
|
|
bool is_down,
|
|
uint32_t scan_code,
|
|
bool extended) {
|
|
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
|
if (captured_keyboard_inputs_.size() >= kMaxCapturedKeyboardInputs) {
|
|
captured_keyboard_inputs_.pop_front();
|
|
LOG_WARN("Captured keyboard input queue overflow, dropping oldest event");
|
|
}
|
|
captured_keyboard_inputs_.push_back(
|
|
CapturedKeyboardInput{key_code, is_down, scan_code, extended});
|
|
}
|
|
|
|
void SessionDeviceManager::DrainCapturedKeyboardInput() {
|
|
std::deque<CapturedKeyboardInput> inputs;
|
|
{
|
|
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
|
inputs.swap(captured_keyboard_inputs_);
|
|
}
|
|
for (const CapturedKeyboardInput &input : inputs) {
|
|
owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down,
|
|
input.scan_code, input.extended);
|
|
}
|
|
}
|
|
|
|
void SessionDeviceManager::ClearCapturedKeyboardInput() {
|
|
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
|
captured_keyboard_inputs_.clear();
|
|
}
|
|
|
|
bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
|
|
uint32_t scan_code,
|
|
bool extended) {
|
|
return keyboard_capturer_ && keyboard_capturer_->SendKeyboardCommand(
|
|
key_code, is_down, scan_code, extended) == 0;
|
|
}
|
|
|
|
void SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
|
|
int selected_display) {
|
|
if (mouse_controller_) {
|
|
mouse_controller_->SendMouseCommand(action, selected_display);
|
|
}
|
|
}
|
|
|
|
int SessionDeviceManager::SwitchDisplay(int display_id) {
|
|
return screen_capturer_ ? screen_capturer_->SwitchTo(display_id) : -1;
|
|
}
|
|
|
|
void SessionDeviceManager::ResetToInitialDisplay() {
|
|
if (screen_capturer_) {
|
|
screen_capturer_->ResetToInitialMonitor();
|
|
}
|
|
}
|
|
|
|
const std::vector<DisplayInfo> &
|
|
SessionDeviceManager::display_info_list() const {
|
|
return display_info_list_;
|
|
}
|
|
|
|
void SessionDeviceManager::DestroyDevices() {
|
|
if (mouse_controller_) {
|
|
mouse_controller_->Destroy();
|
|
delete mouse_controller_;
|
|
mouse_controller_ = nullptr;
|
|
}
|
|
if (screen_capturer_) {
|
|
screen_capturer_->Destroy();
|
|
delete screen_capturer_;
|
|
screen_capturer_ = nullptr;
|
|
}
|
|
if (speaker_capturer_) {
|
|
speaker_capturer_->Destroy();
|
|
delete speaker_capturer_;
|
|
speaker_capturer_ = nullptr;
|
|
}
|
|
if (keyboard_capturer_) {
|
|
delete keyboard_capturer_;
|
|
keyboard_capturer_ = nullptr;
|
|
}
|
|
}
|
|
|
|
void SessionDeviceManager::DestroyFactories() {
|
|
delete screen_capturer_factory_;
|
|
screen_capturer_factory_ = nullptr;
|
|
delete speaker_capturer_factory_;
|
|
speaker_capturer_factory_ = nullptr;
|
|
delete device_controller_factory_;
|
|
device_controller_factory_ = nullptr;
|
|
}
|
|
|
|
} // namespace crossdesk
|