Compare commits

..
Author SHA1 Message Date
dijunkun 16348444f0 [feat] add Windows native video rendering pipeline 2026-09-03 01:53:34 +08:00
76 changed files with 1909 additions and 3599 deletions
-5
View File
@@ -15,7 +15,6 @@
#ifdef _WIN32
#include <cstdio>
#include "platform/windows/gui/slint_backend.h"
#include "service_host.h"
#endif
@@ -176,10 +175,6 @@ int HandleServiceCliCommand(const std::string& command) {
int main(int argc, char* argv[]) {
#ifdef _WIN32
if (argc == 2 &&
std::strcmp(argv[1], crossdesk::kSlintRendererProbeArgument) == 0) {
return crossdesk::RunSlintRendererProbe();
}
if (argc > 1 && IsServiceCliCommand(argv[1])) {
return HandleServiceCliCommand(argv[1]);
}
@@ -1,79 +0,0 @@
#include "task_queue_lock_free.h"
#include <exception>
#include <new>
#include <stdexcept>
#include <utility>
namespace crossdesk {
TaskQueueLockFree::TaskQueueLockFree()
: producer_(task_queue_), worker_([this] { WorkerThread(); }) {}
TaskQueueLockFree::~TaskQueueLockFree() { Stop(); }
std::future<void> TaskQueueLockFree::PostTask(std::function<void()> task) {
TaskItem item;
item.run = std::move(task);
auto result = item.completion.get_future();
{
std::lock_guard lock(mutex_);
if (stopping_) {
throw std::runtime_error("Cannot post to a stopped task queue");
}
// Count before publishing: the worker may dequeue as soon as enqueue returns.
++pending_tasks_;
if (!task_queue_.enqueue(producer_, std::move(item))) {
--pending_tasks_;
throw std::bad_alloc();
}
}
wake_.notify_one();
return result;
}
void TaskQueueLockFree::Stop() {
// Concurrent Stop callers all wait for the same worker shutdown to finish.
std::call_once(stop_once_, [this] {
{
std::lock_guard lock(mutex_);
stopping_ = true;
}
wake_.notify_all();
worker_.join();
});
}
void TaskQueueLockFree::WorkerThread() {
while (true) {
TaskItem item;
while (task_queue_.try_dequeue(item)) {
--pending_tasks_;
std::exception_ptr error;
try {
item.run();
} catch (...) {
error = std::current_exception();
}
// Release captured resources before marking the task complete.
item.run = nullptr;
if (error) {
item.completion.set_exception(error);
} else {
item.completion.set_value();
}
}
std::unique_lock lock(mutex_);
wake_.wait(lock, [this] { return stopping_ || pending_tasks_ > 0; });
// A stop request must not discard tasks submitted while the worker was idle.
if (stopping_ && pending_tasks_ == 0) {
return;
}
}
}
} // namespace crossdesk
@@ -1,57 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _TASK_QUEUE_LOCK_FREE_H_
#define _TASK_QUEUE_LOCK_FREE_H_
#include <atomic>
#include <condition_variable>
#include <cstddef>
#include <functional>
#include <future>
#include <mutex>
#include <thread>
#include <concurrentqueue.h>
namespace crossdesk {
// A single worker executes tasks in submission order. The concurrent queue
// stores tasks; a mutex coordinates submissions, worker wakeups and shutdown.
class TaskQueueLockFree {
public:
TaskQueueLockFree();
~TaskQueueLockFree();
// Returns completion or the task's exception. Throws if the queue is stopped.
std::future<void> PostTask(std::function<void()> task);
// Rejects new tasks, finishes accepted tasks and joins the worker.
// May be called repeatedly, but never from the queue's own tasks.
void Stop();
private:
struct TaskItem {
std::function<void()> run;
std::promise<void> completion;
};
void WorkerThread();
moodycamel::ConcurrentQueue<TaskItem> task_queue_;
// All submissions share one producer so different caller threads keep FIFO.
moodycamel::ProducerToken producer_;
std::atomic<std::size_t> pending_tasks_{0};
std::mutex mutex_;
std::condition_variable wake_;
bool stopping_ = false;
std::once_flag stop_once_;
std::thread worker_;
};
} // namespace crossdesk
#endif
@@ -1,11 +1,5 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _APPLICATION_STATE_H_
#define _APPLICATION_STATE_H_
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
#define CROSSDESK_GUI_APPLICATION_STATE_H_
#include <atomic>
#include <chrono>
@@ -198,4 +192,4 @@ struct ApplicationState : MainWindowState,
} // namespace crossdesk::gui_detail
#endif
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
@@ -37,7 +37,6 @@
#if _WIN32
#include <windows.h>
#include "platform/windows/gui/slint_backend.h"
#include "platform/windows/gui/tray/win_tray.h"
#elif defined(__APPLE__)
#include "platform/macos/gui/tray/mac_tray.h"
@@ -48,7 +47,6 @@
#include <X11/Xlib.h>
#include <unistd.h>
#include "platform/linux/gui/cuda_graphics.h"
#include "platform/linux/gui/tray/linux_tray.h"
#endif
#include "rd_log.h"
@@ -994,15 +992,6 @@ int GuiApplication::Run() {
if (!InitializeSDL()) {
return -1;
}
#if _WIN32
const auto backend = ConfigureWindowsSlintBackend();
if (!backend.success) {
LOG_ERROR("{}", backend.diagnostic);
SDL_Quit();
return -1;
}
LOG_INFO("Slint backend: {}; {}", backend.backend, backend.diagnostic);
#endif
InitializeModules();
InitializeUi();
@@ -1031,7 +1020,6 @@ void GuiApplication::InitializeSettings() {
bool GuiApplication::InitializeSDL() {
#if defined(__linux__) && !defined(__APPLE__)
ConfigureLinuxCudaGraphics(config_center_->IsHardwareVideoCodec());
// Standard Wayland top-level windows cannot choose an absolute screen
// position. Use the session's XWayland compatibility server for the GUI so
// auxiliary windows such as the controlled-side status window can be
@@ -1740,7 +1728,7 @@ void GuiApplication::BindServerCallbacks() {
});
server->on_disconnect_controller([this] {
if (peer_ && !selected_server_remote_id_.empty()) {
CloseServerController(selected_server_remote_id_);
LeaveConnection(peer_, selected_server_remote_id_.c_str());
}
});
}
@@ -1750,7 +1738,6 @@ void GuiApplication::Tick() {
slint::quit_event_loop();
return;
}
HandleSessionCleanup();
HandlePasswordChangeResult();
HandleCredentialRecovery();
if (!peer_) {
@@ -2011,7 +1998,8 @@ void GuiApplication::HandlePasswordChangeResult() {
}
show_offline_warning_window_ = true;
if (uncertain && peer_) {
CloseConnectionPeer();
LeaveConnection(peer_, client_id_);
DestroyPeer(&peer_);
}
return;
}
@@ -2024,7 +2012,8 @@ void GuiApplication::HandlePasswordChangeResult() {
LOG_INFO("Password changed successfully for [{}]", client_id_);
if (peer_) {
CloseConnectionPeer();
LeaveConnection(peer_, client_id_);
DestroyPeer(&peer_);
}
}
@@ -2048,7 +2037,7 @@ void GuiApplication::HandleCredentialRecovery() {
if (retry_active) {
LOG_INFO("Pending credential was not accepted; retrying active credential");
if (peer_) {
CloseConnectionPeer();
DestroyPeer(&peer_);
}
return;
}
@@ -3084,6 +3073,9 @@ void GuiApplication::CloseStreamTab(const std::string& remote_id) {
}
keyboard_.ReleaseRemotePressedKeys(remote_id, "stream_tab_closed");
CloseRemoteSession(props);
WaitForThumbnailSaveTasks();
reload_recent_connections_ = true;
recent_connection_image_save_time_ = 0;
ResetRemoteSessionResources(props);
{
std::unique_lock lock(remote_sessions_mutex_);
@@ -3288,7 +3280,7 @@ void GuiApplication::Cleanup() {
devices_.DestroyDevices();
devices_.DestroyFactories();
CloseAllRemoteSessions();
WaitForSessionCleanup();
WaitForThumbnailSaveTasks();
devices_.DestroyAudioOutput();
if (ui_->stream) {
#if defined(__APPLE__)
@@ -1,9 +1,3 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _GUI_APPLICATION_H_
#define _GUI_APPLICATION_H_
@@ -100,4 +94,4 @@ private:
} // namespace crossdesk
#endif
#endif
@@ -1,11 +1,4 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SERVER_WINDOW_STATE_H_
#define _SERVER_WINDOW_STATE_H_
#pragma once
#include <algorithm>
#include <iterator>
@@ -31,6 +24,5 @@ inline int ReconcileSelectedController(const std::vector<std::string>& ids,
}
return static_cast<int>(std::distance(ids.begin(), selected));
}
} // namespace crossdesk::server_window_state
#endif
} // namespace crossdesk::server_window_state
@@ -1,11 +1,4 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOW_GEOMETRY_H_
#define _WINDOW_GEOMETRY_H_
#pragma once
#include <algorithm>
@@ -45,5 +38,3 @@ constexpr PhysicalPosition BottomRightPosition(const PhysicalRect& bounds,
}
} // namespace crossdesk::window_geometry
#endif
@@ -74,8 +74,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
const int init_ret = screen_capturer_->Init(
fps, [this, fps](unsigned char *data, int size, int width, int height,
const char *display_name,
const MiniRtcNativeVideoFrame *native_frame) {
const char *display_name) {
const auto now_time = std::chrono::steady_clock::now();
if (!ShouldSendCapturedFrame(now_time, fps)) {
return;
@@ -137,13 +136,12 @@ int SessionDeviceManager::InitializeScreenCapturer() {
}
}
MiniRtcVideoFrame frame{};
XVideoFrame frame{};
frame.data = reinterpret_cast<const char *>(data);
frame.size = size;
frame.width = width;
frame.height = height;
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
frame.native_frame = native_frame;
for (const std::string &remote_id : connected_remote_ids) {
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
remote_id.data(), remote_id.size());
@@ -206,11 +204,8 @@ int SessionDeviceManager::StartSpeakerCapturer() {
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) {
MiniRtcAudioFrame frame{};
frame.data = reinterpret_cast<const char *>(data);
frame.size = size;
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
SendAudioFrame(owner_.peer_, &frame, owner_.audio_label_.c_str());
SendAudioFrame(owner_.peer_, reinterpret_cast<const char *>(data),
size, owner_.audio_label_.c_str());
});
if (init_ret != 0) {
@@ -20,16 +20,6 @@ namespace crossdesk {
FileTransferManager::FileTransferManager(GuiRuntime &owner) : owner_(owner) {}
FileTransferManager::~FileTransferManager() {
std::vector<std::future<void>> workers;
{
std::lock_guard lock(workers_mutex_);
stopping_ = true;
workers.swap(workers_);
}
for (auto& worker : workers) worker.wait();
}
FileTransferManager::FileTransferState &FileTransferManager::global_state() {
return global_state_;
}
@@ -102,16 +92,8 @@ void FileTransferManager::Start(
std::shared_ptr<RemoteSession> props,
const std::filesystem::path &file_path, const std::string &file_label,
const std::string &remote_id) {
if (props && props->closing_) return;
const bool is_global = !props;
PeerPtr* peer = nullptr;
std::shared_ptr<PeerEventHandler> peer_events;
{
std::shared_lock lock(owner_.remote_sessions_mutex_);
if (props && props->closing_) return;
peer = is_global ? owner_.peer_ : props->peer_;
peer_events = is_global ? owner_.peer_events_ : props->peer_events_;
}
PeerPtr *peer = is_global ? owner_.peer_ : props->peer_;
if (!peer) {
LOG_ERROR("StartFileTransfer: invalid peer");
return;
@@ -126,14 +108,7 @@ void FileTransferManager::Start(
}
const auto props_weak = std::weak_ptr<RemoteSession>(props);
std::lock_guard workers_lock(workers_mutex_);
if (stopping_) return;
// Reap finished senders without blocking the UI.
std::erase_if(workers_, [](auto& worker) {
return worker.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
});
workers_.push_back(std::async(std::launch::async,
[this, peer, peer_events, file_path, file_label, props_weak, remote_id,
std::thread([this, peer, file_path, file_label, props_weak, remote_id,
is_global]() {
auto props_locked = props_weak.lock();
FileTransferState *state = nullptr;
@@ -191,11 +166,7 @@ void FileTransferManager::Start(
const int ret = sender.SendFile(
file_path, file_path.filename().string(),
[peer, peer_events, file_label, remote_id, props_locked](const char *buffer, size_t size) {
// Close drains each in-flight send before detaching the peer.
if (props_locked && props_locked->closing_) return -1;
auto callback = peer_events->EnterCallback();
if (!callback) return -1;
[peer, file_label, remote_id](const char *buffer, size_t size) {
if (remote_id.empty()) {
return SendReliableDataFrame(peer, buffer, size,
file_label.c_str());
@@ -229,13 +200,12 @@ void FileTransferManager::Start(
}
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
file_path.string().c_str(), ret);
if (peer_events->IsActive()) ProcessQueue(props_locked);
}));
ProcessQueue(props_locked);
}).detach();
}
void FileTransferManager::ProcessQueue(
std::shared_ptr<RemoteSession> props) {
if (props && props->closing_) return;
FileTransferState &state = state_for(props);
if (state.file_sending_.load()) {
return;
@@ -4,9 +4,6 @@
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <future>
#include <mutex>
#include <vector>
#include <memory>
#include <shared_mutex>
#include <string>
@@ -24,7 +21,6 @@ public:
using RemoteSession = gui_detail::RemoteSession;
explicit FileTransferManager(GuiRuntime &owner);
~FileTransferManager();
FileTransferState &global_state();
FileTransferState &
@@ -44,9 +40,6 @@ private:
void ProcessQueue(std::shared_ptr<RemoteSession> props);
void Unregister(uint32_t file_id, bool per_peer);
std::mutex workers_mutex_;
std::vector<std::future<void>> workers_;
bool stopping_ = false;
GuiRuntime &owner_;
FileTransferState global_state_;
std::unordered_map<uint32_t, std::weak_ptr<RemoteSession>>
@@ -150,7 +150,6 @@ void KeyboardController::SendHeartbeat(bool force) {
const std::string target_id = owner_.controlled_remote_id_.empty()
? owner_.focused_remote_id_
: owner_.controlled_remote_id_;
std::shared_lock sessions_lock(owner_.remote_sessions_mutex_);
const auto props_it = owner_.remote_sessions_.find(target_id);
if (target_id.empty() || props_it == owner_.remote_sessions_.end() ||
props_it->second->connection_status_.load() !=
@@ -190,7 +189,6 @@ int KeyboardController::SendKeyCommand(int key_code, bool is_down,
const std::string target_id = owner_.controlled_remote_id_.empty()
? owner_.focused_remote_id_
: owner_.controlled_remote_id_;
std::shared_lock sessions_lock(owner_.remote_sessions_mutex_);
const auto props_it = owner_.remote_sessions_.find(target_id);
if (!target_id.empty() && props_it != owner_.remote_sessions_.end() &&
props_it->second->connection_status_.load() ==
+3 -9
View File
@@ -1,11 +1,5 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _RENDER_H_
#define _RENDER_H_
#ifndef CROSSDESK_GUI_RENDER_H_
#define CROSSDESK_GUI_RENDER_H_
#include <memory>
@@ -31,4 +25,4 @@ private:
} // namespace crossdesk
#endif
#endif // CROSSDESK_GUI_RENDER_H_
@@ -119,9 +119,21 @@ struct SlintVideoPresenter::Impl {
return;
}
std::lock_guard lock(gl_mutex);
auto* opengl_renderer =
dynamic_cast<OpenGlVideoRenderer*>(&renderer);
if (state == slint::RenderingState::AfterRendering && stream &&
opengl_renderer &&
(*stream)->get_native_video_enabled() &&
opengl_renderer->ShouldContinueRendering()) {
// Queue the next frame directly from the display callback. A
// 16/17 ms polling timer can drift across vsync and make Windows
// coalesce every second request, effectively capping video near
// 30 fps even when decode and submission stay near 60 fps.
(*stream)->window().request_redraw();
return;
}
std::lock_guard lock(gl_mutex);
if (state == slint::RenderingState::RenderingSetup) {
GLuint texture = 0;
glGenTextures(1, &texture);
@@ -1,11 +1,5 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SLINT_VIDEO_PRESENTER_H_
#define _SLINT_VIDEO_PRESENTER_H_
#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
#include <cstddef>
#include <cstdint>
@@ -78,4 +72,4 @@ private:
} // namespace crossdesk
#endif
#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
@@ -6,7 +6,6 @@
#include <shared_mutex>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include <display_stream_id.h>
@@ -221,11 +220,6 @@ int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
}
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
if (!props || props->closing_.exchange(true)) return;
if (props->peer_events_) props->peer_events_->Deactivate();
props->connection_established_ = false;
props->streaming_ = false;
props->connection_status_.store(ConnectionStatus::Closed);
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
int video_width = 0;
int video_height = 0;
@@ -251,160 +245,85 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
}
}
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
video_height > 0) {
std::vector<unsigned char> buffer_copy(*frame_snapshot);
std::string remote_id = props->remote_id_;
std::string remote_host_name = props->remote_host_name_;
std::string password =
props->remember_password_ ? props->remote_password_ : "";
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
remote_host_name, password,
thumbnail = thumbnail_]() {
thumbnail->SaveToThumbnail((char*)buffer_copy.data(), video_width,
video_height, remote_id, remote_host_name,
password);
});
{
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
thumbnail_save_threads_.emplace_back(std::move(save_thread));
}
}
if (native_renderer) {
native_renderer->DiscardStream(props->remote_id_);
video_frame_dirty_.store(true, std::memory_order_release);
}
PeerPtr* peer = nullptr;
{
// Audio/clipboard senders hold a shared map lock while using this pointer.
std::unique_lock lock(remote_sessions_mutex_);
peer = std::exchange(props->peer_, nullptr);
if (props->peer_) {
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
LeaveConnection(props->peer_, props->remote_id_.c_str());
LOG_INFO("Destroy peer [{}]", props->local_id_);
DestroyPeer(&props->peer_);
}
const auto queued_at = std::chrono::steady_clock::now();
session_cleanup_tasks_[props->remote_id_] = session_cleanup_queue_.PostTask(
[props, peer, frame_snapshot, video_width, video_height,
thumbnail = thumbnail_, queued_at]() mutable {
if (peer) {
LOG_INFO("[{}] Background leave connection [{}]", props->local_id_,
props->remote_id_);
LeaveConnection(peer, props->remote_id_.c_str());
DestroyPeer(&peer);
}
if (thumbnail && frame_snapshot && !frame_snapshot->empty() &&
video_width > 0 && video_height > 0) {
thumbnail->SaveToThumbnail(
reinterpret_cast<char*>(frame_snapshot->data()), video_width,
video_height, props->remote_id_, props->remote_host_name_,
props->remember_password_ ? props->remote_password_ : "");
}
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - queued_at).count();
LOG_INFO("[{}] Session cleanup completed in {} ms", props->remote_id_,
elapsed);
});
}
void GuiRuntime::CloseServerController(const std::string& remote_id) {
if (!peer_ || remote_id.empty() || controller_cleanup_tasks_.count(remote_id))
return;
controller_cleanup_tasks_[remote_id] = session_cleanup_queue_.PostTask(
[peer = peer_, remote_id] { LeaveConnection(peer, remote_id.c_str()); });
}
void GuiRuntime::CloseConnectionPeer() {
if (!peer_) return;
peer_events_->Deactivate();
std::vector<std::string> controllers;
{
std::shared_lock lock(connection_status_mutex_);
for (const auto& [id, status] : connection_status_) controllers.push_back(id);
}
for (const auto& id : controllers)
HandleServerControllerDisconnected(id, "peer_closed");
devices_.StopMouseController();
devices_.StopScreenCapturer();
devices_.StopSpeakerCapturer();
devices_.StopKeyboardCapturer();
signal_connected_ = false;
signal_status_ = SignalStatus::SignalClosed;
PeerPtr* peer;
{
std::unique_lock lock(remote_sessions_mutex_);
peer = std::exchange(peer_, nullptr);
}
// The same serial queue also owns outstanding controller disconnects, so
// these finish before the shared peer is destroyed.
connection_peer_cleanup_ = session_cleanup_queue_.PostTask(
[peer, handler = peer_events_, id = std::string(client_id_)]() mutable {
LOG_INFO("[{}] Background connection peer cleanup", id);
LeaveConnection(peer, id.c_str());
DestroyPeer(&peer);
});
}
void GuiRuntime::CloseAllRemoteSessions() {
CloseConnectionPeer();
is_client_mode_ = false;
pending_reconnects_.clear();
std::vector<std::shared_ptr<RemoteSession>> sessions;
{
std::shared_lock lock(remote_sessions_mutex_);
for (const auto& [id, props] : remote_sessions_) sessions.push_back(props);
if (peer_) {
LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_);
LeaveConnection(peer_, client_id_);
is_client_mode_ = false;
devices_.StopMouseController();
devices_.StopScreenCapturer();
devices_.StopSpeakerCapturer();
devices_.StopKeyboardCapturer();
LOG_INFO("Destroy peer [{}]", client_id_);
DestroyPeer(&peer_);
}
for (const auto& props : sessions) CloseRemoteSession(props);
{
std::unique_lock lock(remote_sessions_mutex_);
// std::shared_lock lock(remote_sessions_mutex_);
for (auto& it : remote_sessions_) {
auto props = it.second;
CloseRemoteSession(props);
}
}
{
// std::unique_lock lock(remote_sessions_mutex_);
remote_sessions_.clear();
}
}
void GuiRuntime::HandleSessionCleanup() {
auto completed = [](std::future<void>& task) {
if (!task.valid() || task.wait_for(std::chrono::seconds(0)) !=
std::future_status::ready) return false;
try {
task.get();
} catch (const std::exception& error) {
LOG_ERROR("Connection cleanup failed: {}", error.what());
}
return true;
};
completed(connection_peer_cleanup_);
for (auto it = controller_cleanup_tasks_.begin();
it != controller_cleanup_tasks_.end();) {
if (completed(it->second)) it = controller_cleanup_tasks_.erase(it);
else ++it;
}
std::vector<std::pair<std::string, PendingReconnect>> reconnects;
for (auto it = session_cleanup_tasks_.begin();
it != session_cleanup_tasks_.end();) {
if (it->second.wait_for(std::chrono::seconds(0)) !=
std::future_status::ready) {
++it;
continue;
}
try {
it->second.get();
} catch (const std::exception& error) {
LOG_ERROR("[{}] Session cleanup failed: {}", it->first, error.what());
}
if (auto pending = pending_reconnects_.find(it->first);
pending != pending_reconnects_.end()) {
reconnects.emplace_back(pending->first, std::move(pending->second));
pending_reconnects_.erase(pending);
}
it = session_cleanup_tasks_.erase(it);
reload_recent_connections_ = true;
recent_connection_image_save_time_ = 0;
}
for (const auto& [id, request] : reconnects) {
ConnectTo(id, request.password.c_str(), request.remember_password);
}
}
void GuiRuntime::WaitForThumbnailSaveTasks() {
std::vector<std::thread> threads_to_join;
void GuiRuntime::WaitForSessionCleanup() {
pending_reconnects_.clear();
auto drain = [](std::future<void>& task) {
if (!task.valid()) return;
try {
task.get();
} catch (const std::exception& error) {
LOG_ERROR("Connection cleanup failed: {}", error.what());
}
};
drain(connection_peer_cleanup_);
for (auto& [id, task] : controller_cleanup_tasks_) drain(task);
controller_cleanup_tasks_.clear();
for (auto& [id, task] : session_cleanup_tasks_) {
try {
task.get();
} catch (const std::exception& error) {
LOG_ERROR("[{}] Session cleanup failed: {}", id, error.what());
{
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
threads_to_join.swap(thumbnail_save_threads_);
}
if (threads_to_join.empty()) {
return;
}
for (auto& thread : threads_to_join) {
if (thread.joinable()) {
thread.join();
}
}
session_cleanup_tasks_.clear();
}
void GuiRuntime::ResetRemoteSessionResources(
@@ -446,11 +365,6 @@ std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check) {
if (session_cleanup_tasks_.count(remote_id)) {
pending_reconnects_[remote_id] = {password ? password : "", remember_password};
LOG_INFO("[{}] Reconnect queued until previous peer is destroyed", remote_id);
return 0;
}
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
int ret =
RequestSingleDevicePresence(remote_id, password, remember_password);
@@ -468,14 +382,16 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
LOG_INFO("Connect to [{}]", remote_id);
focused_remote_id_ = remote_id;
bool exists = FindRemoteSession(remote_id) != nullptr;
// std::shared_lock shared_lock(remote_sessions_mutex_);
bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
// shared_lock.unlock();
if (!exists) {
PeerPtr* peer_to_init = nullptr;
std::string local_id;
{
std::unique_lock unique_lock(remote_sessions_mutex_);
// std::unique_lock unique_lock(remote_sessions_mutex_);
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
auto props = remote_sessions_[remote_id];
@@ -483,8 +399,6 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
props->remote_id_ = remote_id;
memcpy(&props->params_, &params_, sizeof(Params));
props->params_.user_id = props->local_id_.c_str();
props->peer_events_ = std::make_shared<PeerEventHandler>(*this);
props->params_.user_data = props->peer_events_.get();
props->peer_ = CreatePeer(&props->params_);
props->control_window_width_ = title_bar_height_ * 10.0f;
@@ -531,8 +445,8 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
}
int ret = -1;
auto props = FindRemoteSession(remote_id);
if (!props || props->closing_) return -1;
// std::shared_lock read_lock(remote_sessions_mutex_);
auto props = remote_sessions_[remote_id];
if (!props->connection_established_) {
props->connection_status_.store(ConnectionStatus::Connecting);
show_connection_status_window_ = true;
@@ -556,6 +470,7 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
}
}
}
// read_lock.unlock();
return 0;
}
@@ -1,9 +1,3 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CURSOR_POSITION_H_
#define _CURSOR_POSITION_H_
@@ -70,4 +64,4 @@ inline bool NormalizeCursorPosition(
} // namespace crossdesk
#endif
#endif
@@ -1,9 +1,3 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CURSOR_STATE_PROVIDER_H_
#define _CURSOR_STATE_PROVIDER_H_
@@ -36,4 +30,4 @@ class CursorStateProvider {
} // namespace crossdesk
#endif
#endif
@@ -1,11 +1,11 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* @Date: 2026-02-28
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DEVICE_PRESENCE_CACHE_H_
#define _DEVICE_PRESENCE_CACHE_H_
#ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#include <mutex>
#include <string>
@@ -38,4 +38,4 @@ private:
} // namespace crossdesk
#endif
#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
+32 -30
View File
@@ -19,17 +19,11 @@ namespace crossdesk {
GuiRuntime::GuiRuntime()
: clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this),
keyboard_(*this) {}
keyboard_(*this), peer_events_(*this) {}
GuiRuntime::~GuiRuntime() { WaitForSessionCleanup(); }
GuiRuntime::~GuiRuntime() = default;
int GuiRuntime::CreateConnectionPeer() {
// Tick retries after the old peer's leave notification and socket shutdown.
if (connection_peer_cleanup_.valid()) return 0;
{
std::unique_lock lock(remote_sessions_mutex_);
peer_events_ = std::make_shared<PeerEventHandler>(*this);
}
params_.use_cfg_file = false;
std::string signal_server_ip;
@@ -147,10 +141,9 @@ int GuiRuntime::CreateConnectionPeer() {
sizeof(params_.log_path) - 1);
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__)
// Windows and Linux retain pooled CPU NV12 frames or CUDA device frames;
// macOS retains VideoToolbox CVPixelBuffers for direct Metal sampling. All
// platforms fall back to a packed CPU copy when native upload is unavailable.
#if defined(_WIN32)
// Windows accepts ref-counted CPU NV12 and CUDA device NV12. The OpenGL
// renderer retains the decoded frame until the UI thread has uploaded it.
params_.native_video_output = true;
#else
params_.native_video_output = false;
@@ -195,17 +188,14 @@ int GuiRuntime::CreateConnectionPeer() {
params_.on_connection_status = PeerEventHandler::OnConnectionStatus;
params_.on_net_status_report = PeerEventHandler::OnNetStatusReport;
params_.user_data = peer_events_.get();
params_.user_data = &peer_events_;
// The previous peer may have left a terminal status behind. Reset it before
// Init() starts emitting callbacks for the newly selected server.
signal_connected_ = false;
signal_status_ = SignalStatus::SignalConnecting;
{
std::unique_lock lock(remote_sessions_mutex_);
peer_ = CreatePeer(&params_);
}
peer_ = CreatePeer(&params_);
if (peer_) {
LOG_INFO("Create peer instance [{}] successful", client_id_);
Init(peer_);
@@ -249,7 +239,7 @@ void GuiRuntime::UpdateLabels() {
void GuiRuntime::HandleRecentConnections() {
if (session_cleanup_tasks_.empty() && reload_recent_connections_ && thumbnail_) {
if (reload_recent_connections_ && thumbnail_) {
uint32_t now_time = SDL_GetTicks();
if (now_time - recent_connection_image_save_time_ >= 50) {
int ret = thumbnail_->LoadThumbnail(recent_connections_,
@@ -278,22 +268,11 @@ void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
if (1) {
std::shared_lock lock(runtime->remote_sessions_mutex_);
uint64_t captured_timestamp = 0;
for (const auto &it : runtime->remote_sessions_) {
auto props = it.second;
if (props->connection_status_.load() == ConnectionStatus::Connected) {
if (props->peer_) {
if (captured_timestamp == 0) {
const int64_t timestamp_us = GetSystemTimeMicros(props->peer_);
if (timestamp_us > 0) {
captured_timestamp = static_cast<uint64_t>(timestamp_us);
}
}
MiniRtcAudioFrame frame{};
frame.data = reinterpret_cast<const char *>(stream);
frame.size = static_cast<size_t>(len);
frame.captured_timestamp = captured_timestamp;
SendAudioFrame(props->peer_, &frame,
SendAudioFrame(props->peer_, (const char *)stream, len,
runtime->audio_label_.c_str());
}
}
@@ -310,6 +289,29 @@ void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
void GuiRuntime::SdlCaptureAudioOut([[maybe_unused]] void *userdata,
[[maybe_unused]] Uint8 *stream,
[[maybe_unused]] int len) {
// GuiApplication *runtime = (GuiApplication *)userdata;
// for (auto it : runtime->remote_sessions_) {
// auto props = it.second;
// if (props->connection_status_ == SignalStatus::SignalConnected) {
// SendAudioFrame(props->peer_, (const char *)stream, len);
// }
// }
// if (!runtime->audio_buffer_fresh_) {
// return;
// }
// SDL_memset(stream, 0, len);
// if (runtime->audio_len_ == 0) {
// return;
// } else {
// }
// len = (len > runtime->audio_len_ ? runtime->audio_len_ : len);
// SDL_MixAudioFormat(stream, runtime->audio_buffer_, AUDIO_S16LSB, len,
// SDL_MIX_MAXVOLUME);
// runtime->audio_buffer_fresh_ = false;
}
} // namespace crossdesk
+5 -14
View File
@@ -1,11 +1,5 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _GUI_RUNTIME_H_
#define _GUI_RUNTIME_H_
#ifndef CROSSDESK_GUI_RUNTIME_H_
#define CROSSDESK_GUI_RUNTIME_H_
#include <atomic>
#include <memory>
@@ -61,11 +55,8 @@ class GuiRuntime : protected gui_detail::GuiState {
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions();
void CloseConnectionPeer();
void CloseServerController(const std::string& remote_id);
void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
void HandleSessionCleanup();
void WaitForSessionCleanup();
void WaitForThumbnailSaveTasks();
std::shared_ptr<RemoteSession> FindRemoteSession(
const std::string& remote_id);
@@ -92,7 +83,7 @@ class GuiRuntime : protected gui_detail::GuiState {
FileTransferManager transfers_;
SettingsManager settings_;
KeyboardController keyboard_;
std::shared_ptr<PeerEventHandler> peer_events_;
PeerEventHandler peer_events_;
std::atomic<bool> video_frame_dirty_{false};
std::unique_ptr<VideoRenderer> video_renderer_;
@@ -107,4 +98,4 @@ class GuiRuntime : protected gui_detail::GuiState {
} // namespace crossdesk
#endif
#endif // CROSSDESK_GUI_RUNTIME_H_
+8 -6
View File
@@ -1,11 +1,13 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
* Aggregate state visible to GuiRuntime and GuiApplication.
*
* Definitions live beside their owners: SDL/window state in application and
* connection/session state in runtime. This header is intentionally only the
* composition point.
*/
#ifndef _GUI_STATE_H_
#define _GUI_STATE_H_
#ifndef CROSSDESK_GUI_STATE_H_
#define CROSSDESK_GUI_STATE_H_
#include "application/application_state.h"
#include "runtime/runtime_state.h"
@@ -16,4 +18,4 @@ struct GuiState : ApplicationState, RuntimeState {};
} // namespace crossdesk::gui_detail
#endif
#endif // CROSSDESK_GUI_STATE_H_
@@ -79,9 +79,6 @@ void PeerEventHandler::OnReceiveDataBuffer(
const char *data, size_t size, const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
@@ -148,8 +145,9 @@ void PeerEventHandler::OnReceiveDataBuffer(
std::string remote_id(user_id, user_id_size);
if (remote_action.type == ControlType::service_status) {
if (auto props = runtime->FindRemoteSession(remote_id)) {
runtime->ApplyRemoteServiceStatus(*props, remote_action.ss);
auto props_it = runtime->remote_sessions_.find(remote_id);
if (props_it != runtime->remote_sessions_.end()) {
runtime->ApplyRemoteServiceStatus(*props_it->second, remote_action.ss);
}
return;
}
@@ -60,9 +60,6 @@ PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime || !message || size == 0) {
return;
@@ -156,9 +153,6 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
size_t user_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
@@ -204,8 +198,12 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
}
std::string remote_id(client_id.begin() + 2, client_id.end());
auto props = runtime->FindRemoteSession(remote_id);
if (!props) return;
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
auto props = runtime->remote_sessions_.find(remote_id)->second;
props->signal_status_ = status;
if (SignalStatus::SignalConnecting == status) {
props->signal_connected_ = false;
@@ -232,9 +230,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
const size_t user_id_size,
void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) return;
@@ -309,7 +304,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
remote_action_codec::Free(remote_action);
}
if (!runtime->need_to_create_stream_window_) {
if (!runtime->need_to_create_stream_window_ &&
!runtime->remote_sessions_.empty()) {
runtime->need_to_create_stream_window_ = true;
}
props->connection_established_ = true;
@@ -497,12 +493,9 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
void PeerEventHandler::OnNetStatusReport(
const char* client_id, size_t client_id_size, TraversalMode mode,
const MiniRtcNetTrafficStats* net_traffic_stats, const char* user_id,
const XNetTrafficStats* net_traffic_stats, const char* user_id,
const size_t user_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
@@ -563,8 +556,12 @@ void PeerEventHandler::OnNetStatusReport(
}
std::string remote_id(user_id, user_id_size);
auto props = runtime->FindRemoteSession(remote_id);
if (!props) return;
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
auto props = runtime->remote_sessions_.find(remote_id)->second;
if (props->traversal_mode_ != mode) {
props->traversal_mode_ = mode;
LOG_INFO("Net mode: [{}]", int(props->traversal_mode_));
@@ -1,17 +1,10 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PEER_EVENT_HANDLER_H_
#define _PEER_EVENT_HANDLER_H_
#ifndef CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#include <cstddef>
#include <string>
#include "minirtc.h"
#include "runtime/session_lifecycle.h"
namespace crossdesk {
@@ -22,11 +15,7 @@ class PeerEventHandler {
public:
explicit PeerEventHandler(GuiRuntime &owner);
bool IsActive() const { return callbacks_.IsActive(); }
auto EnterCallback() { return callbacks_.Enter(); }
void Deactivate() { callbacks_.Close(); }
static void OnReceiveVideoBuffer(const MiniRtcVideoFrame *video_frame,
static void OnReceiveVideoBuffer(const XVideoFrame *video_frame,
const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size,
void *user_data);
@@ -46,7 +35,7 @@ public:
size_t user_id_size, void *user_data);
static void OnNetStatusReport(const char *client_id, size_t client_id_size,
TraversalMode mode,
const MiniRtcNetTrafficStats *net_traffic_stats,
const XNetTrafficStats *net_traffic_stats,
const char *user_id, size_t user_id_size,
void *user_data);
@@ -54,9 +43,8 @@ private:
static void SendClientInfo(PeerPtr *peer, const std::string &client_id);
GuiRuntime &owner_;
gui_detail::SessionCallbackGate callbacks_;
};
} // namespace crossdesk
#endif
#endif // CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
@@ -14,35 +14,42 @@ namespace {
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
const MiniRtcNativeVideoFrame* GetNativeVideoFrame(const MiniRtcVideoFrame* frame) {
if (!frame || !frame->native_frame) {
#if defined(_WIN32)
const XWindowsVideoFrame* GetWindowsNativeFrame(const XVideoFrame* frame) {
if (!frame ||
frame->native_handle_type != XVideoFrameNativeHandleWindowsNv12 ||
!frame->native_handle) {
return nullptr;
}
const auto* native = frame->native_frame;
const auto* native =
static_cast<const XWindowsVideoFrame*>(frame->native_handle);
return native->struct_size >=
static_cast<uint32_t>(sizeof(MiniRtcNativeVideoFrame)) &&
native->owner && native->copy_to_nv12
static_cast<uint32_t>(sizeof(XWindowsVideoFrame)) &&
native->owner && native->copy_to_cpu
? native
: nullptr;
}
#endif
} // namespace
void PeerEventHandler::OnReceiveVideoBuffer(
const MiniRtcVideoFrame* video_frame, const char* user_id, size_t user_id_size,
const XVideoFrame* video_frame, const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
std::string remote_id(user_id, user_id_size);
auto props = runtime->FindRemoteSession(remote_id);
if (!props) return;
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
GuiRuntime::RemoteSession* props =
runtime->remote_sessions_.find(remote_id)->second.get();
if (props->connection_established_) {
bool background_snapshot_only = false;
@@ -50,10 +57,12 @@ void PeerEventHandler::OnReceiveVideoBuffer(
if (native_renderer && native_renderer->IsActive()) {
VideoRenderer::SubmitResult submit_result =
VideoRenderer::SubmitResult::failed;
if (GetNativeVideoFrame(video_frame)) {
submit_result = native_renderer->SubmitNativeFrame(
remote_id, *video_frame->native_frame);
#if defined(_WIN32)
if (GetWindowsNativeFrame(video_frame)) {
submit_result =
native_renderer->SubmitNativeFrame(remote_id, *video_frame);
} else
#endif
{
submit_result = native_renderer->SubmitNv12(
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
@@ -107,12 +116,14 @@ void PeerEventHandler::OnReceiveVideoBuffer(
}
size_t frame_size = video_frame->size;
const MiniRtcNativeVideoFrame* native_frame =
GetNativeVideoFrame(video_frame);
if (native_frame) {
frame_size = static_cast<size_t>(native_frame->width) *
native_frame->height * 3U / 2U;
#if defined(_WIN32)
const XWindowsVideoFrame* windows_native_frame =
GetWindowsNativeFrame(video_frame);
if (windows_native_frame) {
frame_size = static_cast<size_t>(windows_native_frame->width) *
windows_native_frame->height * 3U / 2U;
}
#endif
if (frame_size == 0) {
return;
}
@@ -126,13 +137,15 @@ void PeerEventHandler::OnReceiveVideoBuffer(
props->back_frame_->resize(frame_size);
}
if (native_frame) {
if (native_frame->copy_to_nv12(
native_frame->owner, props->back_frame_->data(),
#if defined(_WIN32)
if (windows_native_frame) {
if (windows_native_frame->copy_to_cpu(
windows_native_frame->owner, props->back_frame_->data(),
props->back_frame_->size()) != 0) {
return;
}
} else
#endif
{
if (!video_frame->data || video_frame->size < frame_size) {
return;
@@ -172,9 +185,6 @@ void PeerEventHandler::OnReceiveAudioBuffer(
const char* data, size_t size, const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
@@ -1,11 +1,5 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _REMOTE_ACTION_CODEC_H_
#define _REMOTE_ACTION_CODEC_H_
#ifndef CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
#define CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
#include <cstddef>
#include <vector>
@@ -20,4 +14,4 @@ void Free(RemoteAction &action);
} // namespace crossdesk::remote_action_codec
#endif
#endif // CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
+2 -12
View File
@@ -1,9 +1,3 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _REMOTE_SESSION_H_
#define _REMOTE_SESSION_H_
@@ -24,8 +18,6 @@
#include "display_info.h"
#include "minirtc.h"
namespace crossdesk { class PeerEventHandler; }
namespace crossdesk::gui_detail {
struct FileTransferState {
@@ -69,8 +61,6 @@ struct FileTransferState {
struct RemoteSession {
Params params_;
PeerPtr* peer_ = nullptr;
std::shared_ptr<PeerEventHandler> peer_events_;
std::atomic<bool> closing_{false};
std::string audio_label_ = kAudioStream;
std::string data_label_ = kDataStream;
std::string mouse_label_ = kMouseStream;
@@ -172,7 +162,7 @@ struct RemoteSession {
int fps_ = 0;
int frame_count_ = 0;
std::chrono::steady_clock::time_point last_time_;
MiniRtcNetTrafficStats net_traffic_stats_{};
XNetTrafficStats net_traffic_stats_{};
using QueuedFile = FileTransferState::QueuedFile;
using FileTransferStatus = FileTransferState::FileTransferStatus;
@@ -184,4 +174,4 @@ using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
} // namespace crossdesk::gui_detail
#endif
#endif
+3 -18
View File
@@ -1,9 +1,3 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _RUNTIME_STATE_H_
#define _RUNTIME_STATE_H_
@@ -22,11 +16,9 @@
#include <stream_names.h>
#include "config_center.h"
#include "common/task_queue_lock_free.h"
#include "path_manager.h"
#include "runtime/device_presence_cache.h"
#include "runtime/remote_session.h"
#include "runtime/session_lifecycle.h"
#include "thumbnail.h"
namespace crossdesk::gui_detail {
@@ -186,15 +178,8 @@ struct ConnectionState {
std::unordered_map<std::string, RemoteSessionPtr>;
RemoteSessionMap remote_sessions_;
std::shared_mutex remote_sessions_mutex_;
TaskQueueLockFree session_cleanup_queue_;
std::unordered_map<std::string, std::future<void>> session_cleanup_tasks_;
std::future<void> connection_peer_cleanup_;
std::unordered_map<std::string, std::future<void>> controller_cleanup_tasks_;
struct PendingReconnect {
std::string password;
bool remember_password = false;
};
std::unordered_map<std::string, PendingReconnect> pending_reconnects_;
std::vector<std::thread> thumbnail_save_threads_;
std::mutex thumbnail_save_threads_mutex_;
std::shared_mutex connection_status_mutex_;
std::unordered_map<std::string, ConnectionStatus> connection_status_;
@@ -227,4 +212,4 @@ struct RuntimeState : InfrastructureState,
} // namespace crossdesk::gui_detail
#endif
#endif
@@ -1,47 +0,0 @@
#include "runtime/session_lifecycle.h"
namespace crossdesk::gui_detail {
CallbackGuard::CallbackGuard(SessionCallbackGate& callbacks)
: callbacks_(callbacks), accepted_(callbacks.BeginCallback()) {}
CallbackGuard::~CallbackGuard() {
if (accepted_) {
callbacks_.EndCallback();
}
}
bool SessionCallbackGate::IsActive() const {
std::lock_guard lock(mutex_);
return accepting_callbacks_;
}
CallbackGuard SessionCallbackGate::Enter() {
return CallbackGuard(*this);
}
bool SessionCallbackGate::BeginCallback() {
std::lock_guard lock(mutex_);
if (!accepting_callbacks_) {
return false;
}
++running_callbacks_;
return true;
}
void SessionCallbackGate::EndCallback() {
std::lock_guard lock(mutex_);
--running_callbacks_;
if (running_callbacks_ == 0) {
callbacks_finished_.notify_all();
}
}
void SessionCallbackGate::Close() {
std::unique_lock lock(mutex_);
accepting_callbacks_ = false;
// Waiting releases the lock so running callbacks can call EndCallback().
callbacks_finished_.wait(lock, [this] { return running_callbacks_ == 0; });
}
} // namespace crossdesk::gui_detail
@@ -1,58 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SESSION_LIFECYCLE_H_
#define _SESSION_LIFECYCLE_H_
#include <condition_variable>
#include <cstddef>
#include <mutex>
namespace crossdesk::gui_detail {
class SessionCallbackGate;
// Decrements the count on scope exit, including early returns and exceptions.
class CallbackGuard {
public:
explicit CallbackGuard(SessionCallbackGate& callbacks);
~CallbackGuard();
CallbackGuard(const CallbackGuard&) = delete;
CallbackGuard& operator=(const CallbackGuard&) = delete;
explicit operator bool() const { return accepted_; }
private:
SessionCallbackGate& callbacks_;
bool accepted_;
};
// Each peer counts callbacks as they enter and leave.
// Closing rejects new callbacks, then waits for running callbacks to finish.
class SessionCallbackGate {
public:
bool IsActive() const;
CallbackGuard Enter();
// Called by the session owner, never from within this session's callbacks.
void Close();
private:
friend class CallbackGuard;
bool BeginCallback();
void EndCallback();
mutable std::mutex mutex_;
std::condition_variable callbacks_finished_;
bool accepting_callbacks_ = true;
std::size_t running_callbacks_ = 0;
};
} // namespace crossdesk::gui_detail
#endif
+3 -10
View File
@@ -1,11 +1,5 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _UI_LOCALIZATION_H_
#define _UI_LOCALIZATION_H_
#ifndef CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
#define CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
#include <slint.h>
@@ -168,5 +162,4 @@ inline int ApplyStreamWindowStrings(
} // namespace crossdesk::ui_localization
#endif
#endif // CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
@@ -1,82 +0,0 @@
#include "platform/common/gui/cuda_driver.h"
#if CROSSDESK_HAS_CUDA_DRIVER
#if defined(_WIN32)
#include <windows.h>
#else
#include <dlfcn.h>
#endif
#include <type_traits>
#include "rd_log.h"
namespace crossdesk {
const CudaDriver* CudaDriver::Get() {
static const CudaDriver driver;
return driver.loaded_ ? &driver : nullptr;
}
void* CudaDriver::Symbol(const char* name) const {
#if defined(_WIN32)
return reinterpret_cast<void*>(
GetProcAddress(static_cast<HMODULE>(module_), name));
#else
return dlsym(module_, name);
#endif
}
CudaDriver::CudaDriver() {
#if defined(_WIN32)
module_ = LoadLibraryW(L"nvcuda.dll");
#else
module_ = dlopen("libcuda.so.1", RTLD_LAZY | RTLD_LOCAL);
#endif
if (!module_) return;
const auto load = [this](auto& function, const char* name) {
function = reinterpret_cast<std::decay_t<decltype(function)>>(Symbol(name));
return function != nullptr;
};
loaded_ = load(init, "cuInit") &&
load(device_get_count, "cuDeviceGetCount") &&
load(device_get, "cuDeviceGet") &&
load(context_create, "cuCtxCreate_v2") &&
load(context_destroy, "cuCtxDestroy_v2") &&
load(context_push, "cuCtxPushCurrent_v2") &&
load(context_pop, "cuCtxPopCurrent_v2") &&
load(error_name, "cuGetErrorName") &&
load(mem_alloc, "cuMemAlloc_v2") &&
load(mem_free, "cuMemFree_v2") &&
load(memcpy_2d, "cuMemcpy2D_v2") &&
load(memcpy_2d_async, "cuMemcpy2DAsync_v2") &&
load(stream_create, "cuStreamCreate") &&
load(stream_destroy, "cuStreamDestroy_v2") &&
load(stream_synchronize, "cuStreamSynchronize") &&
load(register_gl_buffer, "cuGraphicsGLRegisterBuffer") &&
load(unregister_resource, "cuGraphicsUnregisterResource") &&
load(map_resources, "cuGraphicsMapResources") &&
load(unmap_resources, "cuGraphicsUnmapResources") &&
load(mapped_pointer, "cuGraphicsResourceGetMappedPointer_v2");
}
CudaDriver::~CudaDriver() {
if (!module_) return;
#if defined(_WIN32)
FreeLibrary(static_cast<HMODULE>(module_));
#else
dlclose(module_);
#endif
}
bool CudaDriver::Check(CUresult result, const char* operation) const {
if (result == CUDA_SUCCESS) return true;
const char* name = nullptr;
error_name(result, &name);
LOG_WARN("CUDA/OpenGL {} failed: {} ({})", operation,
name ? name : "unknown CUDA error", static_cast<int>(result));
return false;
}
} // namespace crossdesk
#endif
@@ -1,63 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CUDA_DRIVER_H_
#define _CUDA_DRIVER_H_
#if defined(USE_CUDA) && USE_CUDA && \
(defined(_WIN32) || \
(defined(__linux__) && (defined(__x86_64__) || defined(__amd64__))))
#define CROSSDESK_HAS_CUDA_DRIVER 1
#include <cuda.h>
namespace crossdesk {
// Application-owned driver entry points. No codec library or MiniRTC internals
// are involved; the module remains loaded for this singleton's lifetime.
class CudaDriver {
public:
static const CudaDriver* Get();
bool Check(CUresult result, const char* operation) const;
decltype(&cuInit) init = nullptr;
decltype(&cuDeviceGetCount) device_get_count = nullptr;
decltype(&cuDeviceGet) device_get = nullptr;
CUresult(CUDAAPI* context_create)(CUcontext*, unsigned int, CUdevice) = nullptr;
decltype(&cuCtxDestroy) context_destroy = nullptr;
decltype(&cuCtxPushCurrent) context_push = nullptr;
decltype(&cuCtxPopCurrent) context_pop = nullptr;
decltype(&cuGetErrorName) error_name = nullptr;
decltype(&cuMemAlloc) mem_alloc = nullptr;
decltype(&cuMemFree) mem_free = nullptr;
decltype(&cuMemcpy2D) memcpy_2d = nullptr;
decltype(&cuMemcpy2DAsync) memcpy_2d_async = nullptr;
decltype(&cuStreamCreate) stream_create = nullptr;
decltype(&cuStreamDestroy) stream_destroy = nullptr;
decltype(&cuStreamSynchronize) stream_synchronize = nullptr;
CUresult(CUDAAPI* register_gl_buffer)(CUgraphicsResource*, unsigned int,
unsigned int) = nullptr;
decltype(&cuGraphicsUnregisterResource) unregister_resource = nullptr;
decltype(&cuGraphicsMapResources) map_resources = nullptr;
decltype(&cuGraphicsUnmapResources) unmap_resources = nullptr;
decltype(&cuGraphicsResourceGetMappedPointer) mapped_pointer = nullptr;
private:
CudaDriver();
~CudaDriver();
CudaDriver(const CudaDriver&) = delete;
CudaDriver& operator=(const CudaDriver&) = delete;
void* Symbol(const char* name) const;
void* module_ = nullptr;
bool loaded_ = false;
};
} // namespace crossdesk
#else
#define CROSSDESK_HAS_CUDA_DRIVER 0
#endif
#endif
@@ -1,198 +0,0 @@
#include "platform/common/gui/cuda_opengl_interop.h"
#include "platform/common/gui/cuda_driver.h"
#include <memory>
#include <new>
#include "platform/common/gui/native_video_frame_ref.h"
namespace crossdesk {
#if CROSSDESK_HAS_CUDA_DRIVER
namespace {
class ScopedCudaContext {
public:
ScopedCudaContext(const CudaDriver& driver, CUcontext context)
: cuda(driver),
active_(cuda.Check(cuda.context_push(context), "cuCtxPushCurrent")) {}
~ScopedCudaContext() { Pop(); }
explicit operator bool() const { return active_; }
bool Pop() {
if (!active_) return true;
active_ = false;
CUcontext context = nullptr;
return cuda.Check(cuda.context_pop(&context), "cuCtxPopCurrent");
}
private:
const CudaDriver& cuda;
bool active_;
};
bool IsCudaFrame(const MiniRtcNativeVideoFrame* frame) {
return frame && GetOpenGlNativeFrame(*frame) &&
frame->type == MiniRtcNativeVideoFrameCudaNv12;
}
} // namespace
struct CudaOpenGlVideoBuffer {
const CudaDriver* driver = nullptr;
CUcontext context = nullptr;
CUstream stream = nullptr;
CUgraphicsResource resource = nullptr;
bool mapped = false;
bool failed = false;
NativeVideoFrameRef source_frame;
};
bool IsCudaDeviceAvailable() {
const auto* cuda = CudaDriver::Get();
int count = 0;
return cuda && cuda->init(0) == CUDA_SUCCESS &&
cuda->device_get_count(&count) == CUDA_SUCCESS && count > 0;
}
CudaOpenGlVideoBuffer* CreateCudaOpenGlVideoBuffer(
const MiniRtcNativeVideoFrame* frame, uint32_t gl_buffer) {
const auto* driver = CudaDriver::Get();
if (!IsCudaFrame(frame) || gl_buffer == 0 || !driver) {
return nullptr;
}
std::unique_ptr<CudaOpenGlVideoBuffer, decltype(&DestroyCudaOpenGlVideoBuffer)>
buffer(new (std::nothrow) CudaOpenGlVideoBuffer,
&DestroyCudaOpenGlVideoBuffer);
if (!buffer) return nullptr;
buffer->driver = driver;
const auto& cuda = *driver;
buffer->context = static_cast<CUcontext>(frame->payload.cuda_nv12.context);
buffer->source_frame = NativeVideoFrameRef(frame);
ScopedCudaContext context(cuda, buffer->context);
if (!context ||
!cuda.Check(cuda.stream_create(&buffer->stream,
CU_STREAM_NON_BLOCKING),
"cuStreamCreate") ||
!cuda.Check(cuda.register_gl_buffer(
&buffer->resource, gl_buffer,
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD),
"cuGraphicsGLRegisterBuffer") ||
!context.Pop()) {
return nullptr;
}
return buffer.release();
}
int UploadCudaFrameToOpenGlBuffer(
CudaOpenGlVideoBuffer* buffer, const MiniRtcNativeVideoFrame* frame) {
if (!buffer || buffer->failed || !IsCudaFrame(frame) ||
frame->payload.cuda_nv12.context != buffer->context) {
return -1;
}
const auto& cuda = *buffer->driver;
ScopedCudaContext context(cuda, buffer->context);
if (!context) {
buffer->failed = true;
return -1;
}
// The caller has completed this buffer's previous GL use. Hold the new
// surface through asynchronous copies, including partially failed uploads.
buffer->source_frame = NativeVideoFrameRef(frame);
const char* operation = "cuGraphicsMapResources";
CUresult result =
cuda.map_resources(1, &buffer->resource, buffer->stream);
buffer->mapped = result == CUDA_SUCCESS;
CUdeviceptr destination = 0;
size_t mapped_size = 0;
if (result == CUDA_SUCCESS) {
operation = "cuGraphicsResourceGetMappedPointer";
result = cuda.mapped_pointer(
&destination, &mapped_size, buffer->resource);
}
const size_t y_size = static_cast<size_t>(frame->width) * frame->height;
if (result == CUDA_SUCCESS && mapped_size < y_size + y_size / 2) {
operation = "validate mapped buffer size";
result = CUDA_ERROR_INVALID_VALUE;
}
if (result == CUDA_SUCCESS) {
CUDA_MEMCPY2D copy{};
copy.srcMemoryType = CU_MEMORYTYPE_DEVICE;
copy.srcDevice = frame->payload.cuda_nv12.y_device_pointer;
copy.srcPitch = frame->payload.cuda_nv12.y_stride;
copy.dstMemoryType = CU_MEMORYTYPE_DEVICE;
copy.dstDevice = destination;
copy.dstPitch = frame->width;
copy.WidthInBytes = frame->width;
copy.Height = frame->height;
operation = "cuMemcpy2DAsync (Y plane)";
result = cuda.memcpy_2d_async(&copy, buffer->stream);
if (result == CUDA_SUCCESS) {
copy.srcDevice = frame->payload.cuda_nv12.uv_device_pointer;
copy.srcPitch = frame->payload.cuda_nv12.uv_stride;
copy.dstDevice = destination + y_size;
copy.Height = frame->height / 2;
operation = "cuMemcpy2DAsync (UV plane)";
result = cuda.memcpy_2d_async(&copy, buffer->stream);
}
}
if (buffer->mapped) {
const CUresult unmap =
cuda.unmap_resources(1, &buffer->resource, buffer->stream);
buffer->mapped = unmap != CUDA_SUCCESS;
if (result == CUDA_SUCCESS) {
operation = "cuGraphicsUnmapResources";
result = unmap;
} else {
cuda.Check(unmap, "cuGraphicsUnmapResources (cleanup)");
}
}
const bool copied = cuda.Check(result, operation);
const bool popped = context.Pop();
buffer->failed = !copied || !popped;
return buffer->failed ? -1 : 0;
}
void DestroyCudaOpenGlVideoBuffer(CudaOpenGlVideoBuffer* buffer) {
if (!buffer) return;
// Keep the frame's device context alive until after the context is popped.
std::unique_ptr<CudaOpenGlVideoBuffer> owner(buffer);
const auto& cuda = *buffer->driver;
ScopedCudaContext context(cuda, buffer->context);
if (!context) return;
if (buffer->stream) {
cuda.Check(cuda.stream_synchronize(buffer->stream),
"cuStreamSynchronize (destroy)");
}
if (buffer->mapped) {
cuda.Check(cuda.unmap_resources(
1, &buffer->resource, buffer->stream),
"cuGraphicsUnmapResources (destroy)");
cuda.Check(cuda.stream_synchronize(buffer->stream),
"cuStreamSynchronize (unmap)");
}
if (buffer->resource) {
cuda.Check(cuda.unregister_resource(buffer->resource),
"cuGraphicsUnregisterResource");
}
if (buffer->stream) {
cuda.Check(cuda.stream_destroy(buffer->stream), "cuStreamDestroy");
}
}
#else
bool IsCudaDeviceAvailable() { return false; }
CudaOpenGlVideoBuffer* CreateCudaOpenGlVideoBuffer(
const MiniRtcNativeVideoFrame*, uint32_t) {
return nullptr;
}
int UploadCudaFrameToOpenGlBuffer(
CudaOpenGlVideoBuffer*, const MiniRtcNativeVideoFrame*) {
return -1;
}
void DestroyCudaOpenGlVideoBuffer(CudaOpenGlVideoBuffer*) {}
#endif
} // namespace crossdesk
@@ -1,35 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CUDA_OPENGL_INTEROP_H_
#define _CUDA_OPENGL_INTEROP_H_
#include "minirtc.h"
namespace crossdesk {
bool IsCudaDeviceAvailable();
struct CudaOpenGlVideoBuffer;
// These operations require the matching GL context to be current. The caller
// owns the GL buffer and must allocate its storage before registering it.
CudaOpenGlVideoBuffer* CreateCudaOpenGlVideoBuffer(
const MiniRtcNativeVideoFrame* frame, uint32_t gl_buffer);
// Asynchronous, GPU-only NV12 copy. Retains the source through the next upload
// or destruction, including on failure. Before reusing the buffer, the caller
// must wait for the previous GL fence. Frames must use the same CUDA context.
int UploadCudaFrameToOpenGlBuffer(CudaOpenGlVideoBuffer* buffer,
const MiniRtcNativeVideoFrame* frame);
// Finish prior GL use before calling. Waits for CUDA work and releases retained
// frames; the caller may then resize/delete the GL buffer. Accepts null.
void DestroyCudaOpenGlVideoBuffer(CudaOpenGlVideoBuffer* buffer);
} // namespace crossdesk
#endif
@@ -1,99 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _NATIVE_VIDEO_FRAME_REF_H_
#define _NATIVE_VIDEO_FRAME_REF_H_
#include <limits>
#include <utility>
#include "minirtc.h"
namespace crossdesk {
class NativeVideoFrameRef {
public:
NativeVideoFrameRef() = default;
explicit NativeVideoFrameRef(const MiniRtcNativeVideoFrame *frame) {
if (frame && frame->owner && frame->retain && frame->release) {
frame_ = *frame;
frame_.struct_size = sizeof(frame_);
frame_.retain(frame_.owner);
valid_ = true;
}
}
NativeVideoFrameRef(const NativeVideoFrameRef &other)
: NativeVideoFrameRef(other.Get()) {}
NativeVideoFrameRef(NativeVideoFrameRef &&other) noexcept
: frame_(other.frame_), valid_(std::exchange(other.valid_, false)) {
other.frame_ = {};
}
NativeVideoFrameRef &operator=(NativeVideoFrameRef other) noexcept {
Swap(other);
return *this;
}
~NativeVideoFrameRef() { Reset(); }
void Reset() {
if (valid_ && frame_.owner && frame_.release) {
frame_.release(frame_.owner);
}
frame_ = {};
valid_ = false;
}
void Swap(NativeVideoFrameRef &other) noexcept {
std::swap(frame_, other.frame_);
std::swap(valid_, other.valid_);
}
const MiniRtcNativeVideoFrame *Get() const { return valid_ ? &frame_ : nullptr; }
explicit operator bool() const { return valid_; }
private:
MiniRtcNativeVideoFrame frame_{};
bool valid_ = false;
};
inline const MiniRtcNativeVideoFrame *GetOpenGlNativeFrame(
const MiniRtcNativeVideoFrame &frame) {
const MiniRtcNativeVideoFrame *native = &frame;
if (native->struct_size < static_cast<uint32_t>(sizeof(MiniRtcNativeVideoFrame)) ||
!native->owner || !native->retain || !native->release ||
!native->copy_to_nv12 || native->width == 0 || native->height == 0 ||
(native->width & 1U) != 0 || (native->height & 1U) != 0 ||
native->width > static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
native->height > static_cast<uint32_t>(std::numeric_limits<int>::max())) {
return nullptr;
}
if (native->type == MiniRtcNativeVideoFrameCpuNv12) {
return native->payload.cpu_nv12.y_plane &&
native->payload.cpu_nv12.uv_plane &&
native->payload.cpu_nv12.y_stride >= native->width &&
native->payload.cpu_nv12.uv_stride >= native->width
? native
: nullptr;
}
if (native->type == MiniRtcNativeVideoFrameCudaNv12) {
return native->payload.cuda_nv12.y_device_pointer != 0 &&
native->payload.cuda_nv12.uv_device_pointer != 0 &&
native->payload.cuda_nv12.y_stride >= native->width &&
native->payload.cuda_nv12.uv_stride >= native->width &&
native->payload.cuda_nv12.context
? native
: nullptr;
}
return nullptr;
}
} // namespace crossdesk
#endif
File diff suppressed because it is too large Load Diff
@@ -13,10 +13,11 @@
namespace crossdesk {
// NV12 underlay for Slint's FemtoVG OpenGL renderer on Windows and Linux.
// MiniRTC submits cross-platform native NV12 descriptors or tightly packed CPU
// fallback frames from its decode callback thread. CUDA frames rotate through
// fenced OpenGL upload resources; CPU descriptors expose their planes without
// an intermediate queue copy. The UI thread always consumes the newest frame.
// MiniRTC submits either ref-counted Windows native NV12 frames or packed CPU
// NV12 fallback frames from its decode callback thread. CUDA-backed frames use
// CUDA/OpenGL interop; CPU-backed frames are retained without an intermediate
// queue copy. The Slint UI thread updates Y and UV textures and performs color
// conversion and scaling before Slint draws its overlay.
class OpenGlVideoRenderer final : public VideoRenderer {
public:
OpenGlVideoRenderer();
@@ -32,6 +33,11 @@ public:
bool IsReady() const override;
bool IsActive() const override;
// Keeps Slint's native surface on a display-driven redraw loop while decoded
// frames are arriving. This avoids timer/vsync phase drift that otherwise
// causes every second redraw request to be coalesced on Windows.
bool ShouldContinueRendering() const;
// Only the selected stream is queued for upload. Frames for other tabs are
// retained by GuiRuntime at a throttled rate for thumbnail/tab restoration.
bool SetSelectedStream(std::string remote_id) override;
@@ -43,7 +49,7 @@ public:
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data,
size_t size, int width, int height) override;
SubmitResult SubmitNativeFrame(std::string_view remote_id,
const MiniRtcNativeVideoFrame &frame) override;
const XVideoFrame &frame) override;
// Draws below Slint while its OpenGL context is current. All dimensions are
// physical pixels in the window client area. corner_radius_pixels clips the
@@ -1,53 +0,0 @@
#include "platform/linux/gui/cuda_graphics.h"
#include <cstdlib>
#include <initializer_list>
#include "rd_log.h"
#include "platform/common/gui/cuda_opengl_interop.h"
namespace crossdesk {
void ConfigureLinuxCudaGraphics(bool hardware_acceleration) {
#if USE_CUDA && (defined(__x86_64__) || defined(__amd64__))
if (!hardware_acceleration) {
return;
}
// Respect explicit GPU/driver selection, including a request to use Mesa
// or software rendering. Native frames can still use the CPU fallback.
for (const char* name : {"__NV_PRIME_RENDER_OFFLOAD",
"__NV_PRIME_RENDER_OFFLOAD_PROVIDER",
"__GLX_VENDOR_LIBRARY_NAME",
"__EGL_VENDOR_LIBRARY_FILENAMES",
"__EGL_VENDOR_LIBRARY_DIRS", "DRI_PRIME",
"LIBGL_ALWAYS_SOFTWARE",
"MESA_LOADER_DRIVER_OVERRIDE"}) {
const char* value = std::getenv(name);
if (value && value[0] != '\0') {
LOG_INFO("Keeping explicit Linux graphics selection: {}={}", name,
value);
return;
}
}
if (!IsCudaDeviceAvailable()) {
return;
}
// Both EGL and GLX need to render on NVIDIA to share CUDA frame buffers.
// These are process-local, not desktop settings.
// https://download.nvidia.com/XFree86/Linux-x86_64/595.45.04/README/primerenderoffload.html
if (setenv("__NV_PRIME_RENDER_OFFLOAD", "1", 1) != 0 ||
setenv("__GLX_VENDOR_LIBRARY_NAME", "nvidia", 1) != 0) {
LOG_WARN("Unable to select NVIDIA PRIME rendering for CUDA video");
return;
}
LOG_INFO("Selected NVIDIA PRIME rendering for CUDA/OpenGL video interop");
#else
(void)hardware_acceleration;
#endif
}
} // namespace crossdesk
@@ -1,18 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CUDA_GRAPHICS_H_
#define _CUDA_GRAPHICS_H_
namespace crossdesk {
// Call before SDL or Slint initializes EGL/GLX. Hybrid laptops otherwise
// create the GUI context on the integrated GPU while NVDEC uses NVIDIA.
void ConfigureLinuxCudaGraphics(bool hardware_acceleration);
} // namespace crossdesk
#endif
@@ -392,10 +392,7 @@ struct LinuxTrayImpl {
exit_menu_label(GetExitMenuLabel(language_index_value)),
exit_menu_ascii_label(IsAsciiPrintable(exit_menu_label)
? exit_menu_label
: "Exit") {
EnsureTrayIcon();
}
: "Exit") {}
explicit LinuxTrayImpl(std::function<void()> show_window_callback,
std::function<void()> hide_window_callback,
@@ -419,9 +416,7 @@ struct LinuxTrayImpl {
exit_menu_label(GetExitMenuLabel(language_index_value)),
exit_menu_ascii_label(IsAsciiPrintable(exit_menu_label)
? exit_menu_label
: "Exit") {
EnsureTrayIcon();
}
: "Exit") {}
~LinuxTrayImpl() { RemoveTrayIcon(); }
@@ -429,7 +429,7 @@ bool ScreenCapturerDrm::CaptureOutputFrame(const DrmOutput& output,
if (emit_callback && callback_) {
callback_(nv12.data(), static_cast<int>(nv12.size()), capture_width,
capture_height, output.stream_id.c_str(), nullptr);
capture_height, output.stream_id.c_str());
}
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
@@ -50,8 +50,7 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
callback_orig_ = std::move(cb);
callback_ = [this](unsigned char* data, int size, int width, int height,
const char* reported_stream_id,
const MiniRtcNativeVideoFrame* native_frame) {
const char* reported_stream_id) {
const std::string mapped_stream_id = MapStreamId(reported_stream_id);
if (mapped_stream_id.empty()) {
if (!invalid_stream_id_logged_.exchange(true,
@@ -65,8 +64,7 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
}
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
if (callback_orig_) {
callback_orig_(data, size, width, height, mapped_stream_id.c_str(),
native_frame);
callback_orig_(data, size, width, height, mapped_stream_id.c_str());
}
};
@@ -1047,7 +1047,7 @@ void ScreenCapturerWayland::HandlePipeWireBuffer() {
if (callback_) {
const std::string stream_id = MakeDisplayStreamId(0);
callback_(nv12.data(), static_cast<int>(nv12.size()), even_width,
even_height, stream_id.c_str(), nullptr);
even_height, stream_id.c_str());
}
pipewire_last_frame_ms_.store(NowMs());
@@ -321,7 +321,7 @@ void ScreenCapturerX11::OnFrame() {
if (callback_) {
const std::string stream_id = MakeDisplayStreamId(monitor_index);
callback_(nv12.data(), width_ * height_ * 3 / 2, width_, height_,
stream_id.c_str(), nullptr);
stream_id.c_str());
}
XDestroyImage(image);
@@ -12,11 +12,11 @@
namespace crossdesk {
// macOS video compositor used by CrossDesk only. VideoToolbox CVPixelBuffers
// are sampled directly through CVMetalTextureCache; packed CPU NV12 remains a
// fallback for software decoding and cached snapshots. AppKit attachment and
// rendering are main-thread operations, while frame submission is safe from
// MiniRTC's decode callback thread.
// macOS video compositor used by CrossDesk only. MiniRTC continues to expose
// tightly packed CPU NV12 frames; this renderer performs the single required
// copy into shared Metal buffers and converts NV12 to RGB in the fragment
// shader. AppKit attachment and rendering are main-thread operations, while
// SubmitNv12() is safe to call from MiniRTC's decode callback thread.
class MacMetalVideoRenderer final : public VideoRenderer {
public:
MacMetalVideoRenderer();
@@ -37,11 +37,6 @@ class MacMetalVideoRenderer final : public VideoRenderer {
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t* data,
size_t size, int width, int height) override;
// Retains an IOSurface-backed VideoToolbox CVPixelBuffer until the Metal
// command buffer using it has completed.
SubmitResult SubmitNativeFrame(std::string_view remote_id,
const MiniRtcNativeVideoFrame& frame) override;
// Seeds a newly selected stream from its retained CPU snapshot without
// replacing a newer decoded frame that is already queued or rendering.
SubmitResult SubmitCachedNv12(std::string_view remote_id,
@@ -1,7 +1,6 @@
#include "platform/macos/gui/metal_video_renderer.h"
#import <AppKit/AppKit.h>
#import <CoreVideo/CVMetalTextureCache.h>
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
@@ -195,74 +194,6 @@ namespace {
constexpr size_t kFrameSlotCount = 3;
class NativeVideoFrameRef {
public:
NativeVideoFrameRef() = default;
explicit NativeVideoFrameRef(const MiniRtcNativeVideoFrame* frame) {
if (frame && frame->owner && frame->retain && frame->release) {
frame_ = *frame;
frame_.struct_size = sizeof(frame_);
frame_.retain(frame_.owner);
valid_ = true;
}
}
NativeVideoFrameRef(const NativeVideoFrameRef& other)
: NativeVideoFrameRef(other.Get()) {}
NativeVideoFrameRef(NativeVideoFrameRef&& other) noexcept
: frame_(other.frame_), valid_(std::exchange(other.valid_, false)) {
other.frame_ = {};
}
NativeVideoFrameRef& operator=(NativeVideoFrameRef other) noexcept {
Swap(other);
return *this;
}
~NativeVideoFrameRef() { Reset(); }
void Reset() {
if (valid_ && frame_.owner && frame_.release) {
frame_.release(frame_.owner);
}
frame_ = {};
valid_ = false;
}
void Swap(NativeVideoFrameRef& other) noexcept {
std::swap(frame_, other.frame_);
std::swap(valid_, other.valid_);
}
const MiniRtcNativeVideoFrame* Get() const {
return valid_ ? &frame_ : nullptr;
}
explicit operator bool() const { return valid_; }
private:
MiniRtcNativeVideoFrame frame_{};
bool valid_ = false;
};
const MiniRtcNativeVideoFrame* GetMacNativeFrame(
const MiniRtcNativeVideoFrame& frame) {
const MiniRtcNativeVideoFrame* native = &frame;
if (native->struct_size < static_cast<uint32_t>(sizeof(*native)) ||
native->type != MiniRtcNativeVideoFrameCVPixelBuffer ||
!native->payload.cv_pixel_buffer || !native->owner || !native->retain ||
!native->release || !native->copy_to_nv12 || native->width == 0 ||
native->height == 0 || (native->width & 1U) != 0 ||
(native->height & 1U) != 0 ||
native->width > static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
native->height >
static_cast<uint32_t>(std::numeric_limits<int>::max())) {
return nullptr;
}
return native;
}
// Slint's winit backend creates AppKit windows with transparent backing so
// transparent Slint items can be composited. Keep the content out of the
// titlebar and choose whether AppKit's material or the semantic window color
@@ -331,9 +262,6 @@ struct FrameSlot {
id<MTLBuffer> uv_buffer = nil;
id<MTLTexture> y_texture = nil;
id<MTLTexture> uv_texture = nil;
CVMetalTextureRef y_cv_texture = nullptr;
CVMetalTextureRef uv_cv_texture = nullptr;
NativeVideoFrameRef native_frame;
size_t y_stride = 0;
size_t uv_stride = 0;
int width = 0;
@@ -346,15 +274,6 @@ struct FrameSlot {
void ReleaseResources() {
y_texture = nil;
uv_texture = nil;
if (y_cv_texture) {
CFRelease(y_cv_texture);
y_cv_texture = nullptr;
}
if (uv_cv_texture) {
CFRelease(uv_cv_texture);
uv_cv_texture = nullptr;
}
native_frame.Reset();
y_buffer = nil;
uv_buffer = nil;
y_stride = 0;
@@ -471,7 +390,7 @@ fragment half4 nv12_fragment(
- 16.0 / 255.0);
const float2 uv = uv_texture.sample(video_sampler, in.texcoord).rg - 0.5;
// BT.601 limited-range NV12, matching the previous libyuv NV12ToABGR/RAW
// path. MiniRTC's current MiniRtcVideoFrame API does not carry color metadata, so
// path. MiniRTC's current XVideoFrame API does not carry color metadata, so
// retaining the established matrix avoids a platform-only color change.
const float3 rgb = float3(y + 1.59602678 * uv.y,
y - 0.39176229 * uv.x - 0.81296764 * uv.y,
@@ -487,7 +406,6 @@ struct MacMetalVideoRenderer::Impl {
id<MTLDevice> device = nil;
id<MTLCommandQueue> command_queue = nil;
id<MTLRenderPipelineState> pipeline = nil;
CVMetalTextureCacheRef texture_cache = nullptr;
std::shared_ptr<SharedFrameState> frames =
std::make_shared<SharedFrameState>();
NSView* slint_view = nil;
@@ -499,8 +417,6 @@ struct MacMetalVideoRenderer::Impl {
uint64_t rendered_content_generation = 0;
std::shared_ptr<std::atomic<bool>> native_surface_needs_redraw =
std::make_shared<std::atomic<bool>>(true);
std::atomic<bool> native_path_logged{false};
std::atomic<bool> native_fallback_logged{false};
Impl() {
@autoreleasepool {
@@ -514,12 +430,6 @@ struct MacMetalVideoRenderer::Impl {
LOG_ERROR("Metal video renderer unavailable: command queue creation failed");
return;
}
const CVReturn cache_status = CVMetalTextureCacheCreate(
kCFAllocatorDefault, nullptr, device, nullptr, &texture_cache);
if (cache_status != kCVReturnSuccess || !texture_cache) {
LOG_WARN("Metal video renderer could not create CV texture cache: {}",
cache_status);
}
NSError* library_error = nil;
id<MTLLibrary> library =
@@ -556,11 +466,6 @@ struct MacMetalVideoRenderer::Impl {
~Impl() {
DetachViews();
frames.reset();
if (texture_cache) {
CVMetalTextureCacheFlush(texture_cache, 0);
CFRelease(texture_cache);
texture_cache = nullptr;
}
}
bool Ready() const {
@@ -750,142 +655,6 @@ MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNv12(
return SubmitNv12Internal(remote_id, data, size, width, height, true);
}
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNativeFrame(
std::string_view remote_id, const MiniRtcNativeVideoFrame& frame) {
if (!IsReady()) {
return SubmitResult::failed;
}
const MiniRtcNativeVideoFrame* native = GetMacNativeFrame(frame);
if (!native) {
return SubmitResult::failed;
}
{
std::lock_guard lock(impl_->frames->mutex);
if (impl_->frames->selected_stream != remote_id) {
return SubmitResult::not_selected;
}
}
const int width = static_cast<int>(native->width);
const int height = static_cast<int>(native->height);
const auto submit_cpu_fallback = [&]() {
if (!impl_->native_fallback_logged.exchange(true,
std::memory_order_acq_rel)) {
LOG_WARN("macOS native CVPixelBuffer upload unavailable; using CPU NV12 "
"fallback");
}
const size_t y_size = static_cast<size_t>(width) * height;
std::vector<unsigned char> packed_nv12(y_size + y_size / 2);
if (native->copy_to_nv12(native->owner, packed_nv12.data(),
packed_nv12.size()) != 0) {
return SubmitResult::failed;
}
return SubmitNv12(remote_id, packed_nv12.data(), packed_nv12.size(), width,
height);
};
CVPixelBufferRef pixel_buffer =
static_cast<CVPixelBufferRef>(native->payload.cv_pixel_buffer);
const OSType pixel_format = CVPixelBufferGetPixelFormatType(pixel_buffer);
if (!CVPixelBufferIsPlanar(pixel_buffer) ||
CVPixelBufferGetPlaneCount(pixel_buffer) < 2 ||
CVPixelBufferGetWidth(pixel_buffer) != native->width ||
CVPixelBufferGetHeight(pixel_buffer) != native->height ||
(pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)) {
return submit_cpu_fallback();
}
NativeVideoFrameRef retained(native);
if (!retained || !impl_->texture_cache) {
return submit_cpu_fallback();
}
CVMetalTextureRef y_cv_texture = nullptr;
CVReturn y_status = CVMetalTextureCacheCreateTextureFromImage(
kCFAllocatorDefault, impl_->texture_cache, pixel_buffer, nullptr,
MTLPixelFormatR8Unorm, native->width, native->height, 0,
&y_cv_texture);
if (y_status != kCVReturnSuccess || !y_cv_texture) {
return submit_cpu_fallback();
}
CVMetalTextureRef uv_cv_texture = nullptr;
CVReturn uv_status = CVMetalTextureCacheCreateTextureFromImage(
kCFAllocatorDefault, impl_->texture_cache, pixel_buffer, nullptr,
MTLPixelFormatRG8Unorm, native->width / 2U, native->height / 2U, 1,
&uv_cv_texture);
if (uv_status != kCVReturnSuccess || !uv_cv_texture) {
CFRelease(y_cv_texture);
return submit_cpu_fallback();
}
id<MTLTexture> y_texture = CVMetalTextureGetTexture(y_cv_texture);
id<MTLTexture> uv_texture = CVMetalTextureGetTexture(uv_cv_texture);
if (y_texture == nil || uv_texture == nil) {
CFRelease(y_cv_texture);
CFRelease(uv_cv_texture);
return submit_cpu_fallback();
}
auto& frames = *impl_->frames;
std::lock_guard lock(frames.mutex);
if (frames.selected_stream != remote_id) {
CFRelease(y_cv_texture);
CFRelease(uv_cv_texture);
return SubmitResult::not_selected;
}
// Latest-frame-only queue: replace an unconsumed decoded frame to bound
// latency, while never recycling a frame still referenced by the GPU.
FrameSlot* target = nullptr;
for (auto& slot : frames.slots) {
if (slot.use == SlotUse::pending) {
target = &slot;
break;
}
}
if (!target) {
uint64_t oldest_sequence = std::numeric_limits<uint64_t>::max();
for (auto& slot : frames.slots) {
if (slot.use != SlotUse::available) {
continue;
}
if (!slot.valid) {
target = &slot;
break;
}
if (slot.sequence < oldest_sequence) {
oldest_sequence = slot.sequence;
target = &slot;
}
}
}
if (!target) {
CFRelease(y_cv_texture);
CFRelease(uv_cv_texture);
return SubmitResult::dropped;
}
target->ReleaseResources();
target->y_cv_texture = y_cv_texture;
target->uv_cv_texture = uv_cv_texture;
target->y_texture = y_texture;
target->uv_texture = uv_texture;
target->native_frame = std::move(retained);
target->width = width;
target->height = height;
target->remote_id.assign(remote_id);
target->sequence = frames.next_sequence++;
target->use = SlotUse::pending;
target->valid = true;
if (!impl_->native_path_logged.exchange(true, std::memory_order_acq_rel)) {
LOG_INFO("macOS VideoToolbox-to-Metal native NV12 path enabled");
}
return SubmitResult::submitted;
}
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitCachedNv12(
std::string_view remote_id, const uint8_t* data, size_t size, int width,
int height) {
@@ -1276,57 +1045,40 @@ bool MacMetalVideoRenderer::CopyLatestNv12(
return false;
}
NativeVideoFrameRef native_frame;
{
std::lock_guard lock(impl_->frames->mutex);
const FrameSlot* newest = nullptr;
for (const auto& slot : impl_->frames->slots) {
if (!slot.valid || slot.remote_id != remote_id || slot.sequence == 0 ||
(!slot.native_frame &&
(slot.y_buffer == nil || slot.uv_buffer == nil))) {
continue;
}
if (newest == nullptr || slot.sequence > newest->sequence) {
newest = &slot;
}
std::lock_guard lock(impl_->frames->mutex);
const FrameSlot* newest = nullptr;
for (const auto& slot : impl_->frames->slots) {
if (!slot.valid || slot.remote_id != remote_id || slot.y_buffer == nil ||
slot.uv_buffer == nil || slot.sequence == 0) {
continue;
}
if (newest == nullptr || newest->width <= 0 || newest->height <= 0) {
return false;
}
*width = newest->width;
*height = newest->height;
native_frame = newest->native_frame;
if (!native_frame) {
const size_t y_size =
static_cast<size_t>(newest->width) * newest->height;
output->resize(y_size + y_size / 2);
auto* y_source =
static_cast<const uint8_t*>(newest->y_buffer.contents);
auto* uv_source =
static_cast<const uint8_t*>(newest->uv_buffer.contents);
for (int row = 0; row < newest->height; ++row) {
std::memcpy(output->data() +
static_cast<size_t>(row) * newest->width,
y_source + static_cast<size_t>(row) * newest->y_stride,
newest->width);
}
uint8_t* uv_destination = output->data() + y_size;
for (int row = 0; row < newest->height / 2; ++row) {
std::memcpy(uv_destination +
static_cast<size_t>(row) * newest->width,
uv_source + static_cast<size_t>(row) * newest->uv_stride,
newest->width);
}
return true;
if (newest == nullptr || slot.sequence > newest->sequence) {
newest = &slot;
}
}
if (newest == nullptr || newest->width <= 0 || newest->height <= 0) {
return false;
}
const MiniRtcNativeVideoFrame* native = native_frame.Get();
const size_t y_size = static_cast<size_t>(native->width) * native->height;
const size_t y_size =
static_cast<size_t>(newest->width) * newest->height;
output->resize(y_size + y_size / 2);
return native->copy_to_nv12(native->owner, output->data(), output->size()) ==
0;
auto* y_source = static_cast<const uint8_t*>(newest->y_buffer.contents);
auto* uv_source = static_cast<const uint8_t*>(newest->uv_buffer.contents);
for (int row = 0; row < newest->height; ++row) {
std::memcpy(output->data() + static_cast<size_t>(row) * newest->width,
y_source + static_cast<size_t>(row) * newest->y_stride,
newest->width);
}
uint8_t* uv_destination = output->data() + y_size;
for (int row = 0; row < newest->height / 2; ++row) {
std::memcpy(uv_destination + static_cast<size_t>(row) * newest->width,
uv_source + static_cast<size_t>(row) * newest->uv_stride,
newest->width);
}
*width = newest->width;
*height = newest->height;
return true;
}
} // namespace crossdesk
@@ -42,65 +42,6 @@ static std::string NSErrorToString(NSError *error) {
return description ? description : "";
}
static void RetainCapturedPixelBuffer(void *owner) {
if (owner) {
CVPixelBufferRetain(static_cast<CVPixelBufferRef>(owner));
}
}
static void ReleaseCapturedPixelBuffer(void *owner) {
if (owner) {
CVPixelBufferRelease(static_cast<CVPixelBufferRef>(owner));
}
}
static int CopyCapturedPixelBufferToNv12(void *owner, uint8_t *destination,
size_t destination_size) {
auto pixel_buffer = static_cast<CVPixelBufferRef>(owner);
if (!pixel_buffer || !destination ||
!CVPixelBufferIsPlanar(pixel_buffer) ||
CVPixelBufferGetPlaneCount(pixel_buffer) < 2) {
return -1;
}
const OSType pixel_format = CVPixelBufferGetPixelFormatType(pixel_buffer);
if (pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
return -1;
}
const size_t width = CVPixelBufferGetWidth(pixel_buffer);
const size_t height = CVPixelBufferGetHeight(pixel_buffer);
const size_t required_size = width * height * 3U / 2U;
if (destination_size < required_size ||
CVPixelBufferLockBaseAddress(pixel_buffer,
kCVPixelBufferLock_ReadOnly) !=
kCVReturnSuccess) {
return -1;
}
const auto *y_plane = static_cast<const uint8_t *>(
CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 0));
const auto *uv_plane = static_cast<const uint8_t *>(
CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 1));
const size_t y_stride = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 0);
const size_t uv_stride = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 1);
int result = 0;
if (!y_plane || !uv_plane || y_stride < width || uv_stride < width) {
result = -1;
} else {
for (size_t row = 0; row < height; ++row) {
std::memcpy(destination + row * width, y_plane + row * y_stride, width);
}
uint8_t *uv_destination = destination + width * height;
for (size_t row = 0; row < height / 2U; ++row) {
std::memcpy(uv_destination + row * width,
uv_plane + row * uv_stride, width);
}
}
CVPixelBufferUnlockBaseAddress(pixel_buffer, kCVPixelBufferLock_ReadOnly);
return result;
}
// The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture
// was reported to be broken before macOS 13 - see http://crbug.com/40234870.
// Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were
@@ -151,11 +92,14 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
private:
std::vector<DisplayInfo> display_info_list_;
std::map<int, CGDirectDisplayID> display_id_map_;
unsigned char *nv12_frame_ = nullptr;
size_t nv12_frame_size_ = 0;
int width_ = 0;
int height_ = 0;
int fps_ = 60;
bool show_cursor_ = false;
bool capture_requested_ = false;
bool invalid_stream_id_logged_ = false;
bool native_output_logged_ = false;
public:
// Called by SckHelper when shareable content is returned by ScreenCaptureKit. `content` will be
@@ -175,8 +119,8 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
// object. The helper may outlive this C++ instance, if a completion-handler is passed to
// ScreenCaptureKit APIs and the C++ object is deleted before the handler executes.
SckHelper *__strong helper_ = nil;
// Callback for returning captured frames to the caller. ScreenCaptureKit may
// invoke it on its sample-handler queue.
// Callback for returning captured frames, or errors, to the caller. Only used on the caller's
// thread.
cb_desktop_data _on_data = nullptr;
// Signals that a permanent error occurred. This may be set on any thread, and is read by
// CaptureFrame() which runs on the caller's thread.
@@ -300,6 +244,12 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
display_info_list_.clear();
display_id_map_.clear();
if (nv12_frame_) {
delete[] nv12_frame_;
nv12_frame_ = nullptr;
nv12_frame_size_ = 0;
}
}
int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
@@ -312,7 +262,6 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
fps_ = fps > 0 ? fps : 60;
display_info_list_.clear();
display_id_map_.clear();
native_output_logged_ = false;
if (@available(macOS 10.15, *)) {
bool has_permission = CGPreflightScreenCaptureAccess();
@@ -647,69 +596,81 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
return;
}
const size_t width = CVPixelBufferGetWidth(pixelBuffer);
const size_t height = CVPixelBufferGetHeight(pixelBuffer);
const OSType pixel_format = CVPixelBufferGetPixelFormatType(pixelBuffer);
if (width == 0 || height == 0 || (width & 1U) != 0 ||
(height & 1U) != 0 || !CVPixelBufferIsPlanar(pixelBuffer) ||
CVPixelBufferGetPlaneCount(pixelBuffer) < 2 ||
(pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)) {
LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, format={}, planes={}",
width, height, pixel_format,
size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(pixelBuffer);
if (width == 0 || height == 0 || CVPixelBufferGetPlaneCount(pixelBuffer) < 2) {
LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, planes={}", width, height,
CVPixelBufferGetPlaneCount(pixelBuffer));
return;
}
const size_t required_size = width * height * 3U / 2U;
if (required_size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
LOG_ERROR("Captured frame is too large: {} bytes", required_size);
CVReturn status = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
if (status != kCVReturnSuccess) {
LOG_ERROR("Failed to lock CVPixelBuffer base address: %d", status);
return;
}
cb_desktop_data callback;
std::string stream_id;
bool log_native_output = false;
{
std::lock_guard<std::mutex> lock(lock_);
if (!_on_data || !capture_requested_) {
return;
}
stream_id = ResolveDisplayStreamId(current_stream_id_.c_str(),
display_info_list_.size(),
current_monitor_index_);
if (stream_id.empty()) {
if (!invalid_stream_id_logged_) {
LOG_ERROR("Dropping macOS frames without a registered stream id, "
"display_id={}",
current_display_);
invalid_stream_id_logged_ = true;
}
return;
}
invalid_stream_id_logged_ = false;
callback = _on_data;
if (!native_output_logged_) {
native_output_logged_ = true;
log_native_output = true;
}
size_t required_size = width * height * 3 / 2;
if (required_size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
LOG_ERROR("Captured frame is too large: {} bytes", required_size);
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
MiniRtcNativeVideoFrame native_frame{};
native_frame.struct_size = sizeof(native_frame);
native_frame.type = MiniRtcNativeVideoFrameCVPixelBuffer;
native_frame.width = static_cast<uint32_t>(width);
native_frame.height = static_cast<uint32_t>(height);
native_frame.payload.cv_pixel_buffer = pixelBuffer;
native_frame.owner = pixelBuffer;
native_frame.retain = &RetainCapturedPixelBuffer;
native_frame.release = &ReleaseCapturedPixelBuffer;
native_frame.copy_to_nv12 = &CopyCapturedPixelBufferToNv12;
if (log_native_output) {
LOG_INFO("macOS ScreenCaptureKit native CVPixelBuffer output enabled");
std::lock_guard<std::mutex> lock(lock_);
if (!_on_data || !capture_requested_) {
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
callback(nullptr, static_cast<int>(required_size), static_cast<int>(width),
static_cast<int>(height), stream_id.c_str(), &native_frame);
if (!nv12_frame_ || nv12_frame_size_ < required_size) {
delete[] nv12_frame_;
nv12_frame_ = new unsigned char[required_size];
nv12_frame_size_ = required_size;
}
width_ = static_cast<int>(width);
height_ = static_cast<int>(height);
void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
size_t stride_uv = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
if (!base_y || !base_uv || stride_y < width || stride_uv < width) {
LOG_ERROR("Invalid CVPixelBuffer planes: base_y={}, base_uv={}, stride_y={}, stride_uv={}",
base_y != nullptr, base_uv != nullptr, stride_y, stride_uv);
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
unsigned char *dst_y = nv12_frame_;
for (size_t row = 0; row < height; ++row) {
memcpy(dst_y + row * width, static_cast<unsigned char *>(base_y) + row * stride_y, width);
}
unsigned char *dst_uv = nv12_frame_ + width * height;
for (size_t row = 0; row < height / 2; ++row) {
memcpy(dst_uv + row * width, static_cast<unsigned char *>(base_uv) + row * stride_uv, width);
}
const std::string stream_id = ResolveDisplayStreamId(
current_stream_id_.c_str(), display_info_list_.size(),
current_monitor_index_);
if (stream_id.empty()) {
if (!invalid_stream_id_logged_) {
LOG_ERROR("Dropping macOS frames without a registered stream id, "
"display_id={}",
current_display_);
invalid_stream_id_logged_ = true;
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
invalid_stream_id_logged_ = false;
_on_data(nv12_frame_, static_cast<int>(required_size), static_cast<int>(width),
static_cast<int>(height), stream_id.c_str());
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
}
void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
@@ -1,12 +1,3 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SYSTEM_INFO_H_
#define _SYSTEM_INFO_H_
#include "platform.h"
#include <ifaddrs.h>
@@ -62,6 +53,3 @@ std::string GetHostName() {
bool IsWaylandSession() { return false; }
} // namespace crossdesk
#endif
+1 -3
View File
@@ -58,10 +58,8 @@ public:
virtual SubmitResult SubmitCachedNv12(std::string_view remote_id,
const uint8_t* data, size_t size,
int width, int height) = 0;
// Retains a platform-native decoded frame when the backend supports its
// storage and handle types. Unsupported backends keep the default fallback.
virtual SubmitResult SubmitNativeFrame(std::string_view,
const MiniRtcNativeVideoFrame&) {
const XVideoFrame&) {
return SubmitResult::failed;
}
@@ -1,109 +0,0 @@
#include "platform/windows/gui/slint_backend.h"
#include <windows.h>
#include <cstdlib>
#include <iomanip>
#include <memory>
#include <sstream>
#include <string>
#include <string_view>
namespace crossdesk {
namespace {
struct HandleCloser {
void operator()(HANDLE handle) const noexcept {
if (handle != INVALID_HANDLE_VALUE) {
CloseHandle(handle);
}
}
};
using ScopedHandle = std::unique_ptr<void, HandleCloser>;
std::string ErrorCode(DWORD code) {
std::ostringstream text;
text << "0x" << std::hex << std::uppercase << code;
return text.str();
}
std::string ProbeSlintRenderer(DWORD timeout_ms) {
std::wstring executable(32768, L'\0');
const DWORD length = GetModuleFileNameW(
nullptr, executable.data(), static_cast<DWORD>(executable.size()));
if (length == 0 || length >= executable.size()) {
return "Unable to locate renderer probe executable";
}
executable.resize(length);
// A job also cleans up the probe if the application is closed while the
// graphics driver is stuck. Create it suspended so assignment cannot race.
ScopedHandle job(CreateJobObjectW(nullptr, nullptr));
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {};
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!job ||
!SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation,
&limits, sizeof(limits))) {
return "Unable to create renderer probe job: " + ErrorCode(GetLastError());
}
constexpr std::string_view argument = kSlintRendererProbeArgument;
std::wstring command = L"\"" + executable + L"\" ";
command.append(argument.begin(), argument.end());
STARTUPINFOW startup = {sizeof(startup)};
PROCESS_INFORMATION process = {};
if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr,
FALSE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr,
nullptr, &startup, &process)) {
return "Unable to start renderer probe: " + ErrorCode(GetLastError());
}
ScopedHandle process_handle(process.hProcess);
ScopedHandle thread_handle(process.hThread);
if (!AssignProcessToJobObject(job.get(), process.hProcess) ||
ResumeThread(process.hThread) == static_cast<DWORD>(-1)) {
const DWORD error = GetLastError();
TerminateProcess(process.hProcess, 1);
return "Unable to run renderer probe: " + ErrorCode(error);
}
const DWORD wait = WaitForSingleObject(process.hProcess, timeout_ms);
if (wait != WAIT_OBJECT_0) {
// Closing the job kills only our probe, including any stuck driver thread.
return wait == WAIT_TIMEOUT ? "Slint renderer probe timed out"
: "Waiting for renderer probe failed: " +
ErrorCode(GetLastError());
}
DWORD exit_code = 0;
if (!GetExitCodeProcess(process.hProcess, &exit_code)) {
return "Unable to read renderer probe result: " + ErrorCode(GetLastError());
}
return exit_code == 0
? std::string{}
: "Slint renderer probe exited with " + ErrorCode(exit_code);
}
} // namespace
SlintBackendSelection ConfigureWindowsSlintBackend(uint32_t probe_timeout_ms) {
if (const char* requested = std::getenv("SLINT_BACKEND");
requested && requested[0] != '\0') {
return {true, requested, "Using explicitly requested Slint backend"};
}
// Slint creates its own GL context. A successful SDL/WGL capability check
// cannot validate it, and a Rust panic cannot be caught by C++ exceptions.
// Exercise Slint in a separate process without touching GL in this process.
const std::string error = ProbeSlintRenderer(probe_timeout_ms);
if (error.empty()) {
return {true, "default", "Slint renderer probe rendered successfully"};
}
// _putenv_s updates the CRT and the OS environment read by slint_cpp.dll.
// This is process-local so a later launch can retry hardware rendering.
if (_putenv_s("SLINT_BACKEND", "winit-software") != 0) {
return {false, {}, "Unable to select Slint software rendering: " + error};
}
return {true, "winit-software", error};
}
} // namespace crossdesk
@@ -1,33 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SLINT_BACKEND_H_
#define _SLINT_BACKEND_H_
#include <cstdint>
#include <string>
namespace crossdesk {
struct SlintBackendSelection {
bool success = true;
std::string backend;
std::string diagnostic;
};
inline constexpr char kSlintRendererProbeArgument[] = "--slint-renderer-probe";
// Call before creating any Slint components. The executable's main() must
// dispatch kSlintRendererProbeArgument before services, settings or the daemon.
SlintBackendSelection ConfigureWindowsSlintBackend(
uint32_t probe_timeout_ms = 10000);
// Internal child-process entry point; never call in the main application.
int RunSlintRendererProbe();
} // namespace crossdesk
#endif
@@ -1,59 +0,0 @@
#include <SDL3/SDL.h>
#include <windows.h>
#include <chrono>
#include <iostream>
#include "crossdesk_ui.h"
#include "platform/windows/gui/slint_backend.h"
namespace crossdesk {
int RunSlintRendererProbe() {
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
if (!SDL_Init(SDL_INIT_VIDEO)) {
return 1;
}
int result = 2;
{
// Reuse the application's single AOT entry point. Independently generated
// headers can contain colliding internal component names across TUs.
auto probe = ui::MainWindow::create();
probe->set_custom_titlebar(true);
// AfterRendering confirms that the real GL context, shader initialization
// and first draw all succeeded. GPU readback from an off-screen window can
// be black even when rendering works, so do not use its pixels as a test.
const auto notifier_error = probe->window().set_rendering_notifier(
[&](slint::RenderingState state, slint::GraphicsAPI) {
if (state == slint::RenderingState::AfterRendering) {
result = 0;
}
});
probe->window().set_position(
slint::PhysicalPosition(slint::Point<int32_t>{-32000, -32000}));
probe->show();
slint::Timer timer;
timer.start(slint::TimerMode::SingleShot, std::chrono::milliseconds(100),
[&] {
// Renderers without a rendering notifier (software) support
// snapshots.
if (notifier_error) {
const auto pixels = probe->window().take_snapshot();
if (pixels && pixels->width() > 0 && pixels->height() > 0) {
result = 0;
} else {
std::cerr << "Slint probe could not read rendered pixels"
<< std::endl;
}
}
slint::quit_event_loop();
});
slint::run_event_loop();
probe->hide();
}
SDL_Quit();
return result;
}
} // namespace crossdesk
@@ -60,8 +60,6 @@ WinTray::WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
nid_.uCallbackMessage = WM_TRAY_CALLBACK;
nid_.hIcon = icon_;
wcsncpy_s(nid_.szTip, tip_.c_str(), _TRUNCATE);
EnsureTrayIcon();
}
WinTray::WinTray(std::function<void()> show_window,
@@ -82,7 +80,7 @@ WinTray::~WinTray() {
}
void WinTray::MinimizeToTray() {
EnsureTrayIcon();
Shell_NotifyIcon(NIM_ADD, &nid_);
if (hide_window_) {
hide_window_();
} else if (app_hwnd_) {
@@ -90,25 +88,7 @@ void WinTray::MinimizeToTray() {
}
}
bool WinTray::EnsureTrayIcon() {
if (tray_icon_added_) {
// NIM_MODIFY also detects an icon lost when Explorer was restarted.
if (Shell_NotifyIcon(NIM_MODIFY, &nid_)) {
return true;
}
tray_icon_added_ = false;
}
tray_icon_added_ = Shell_NotifyIcon(NIM_ADD, &nid_) != FALSE;
return tray_icon_added_;
}
void WinTray::RemoveTrayIcon() {
if (tray_icon_added_) {
Shell_NotifyIcon(NIM_DELETE, &nid_);
tray_icon_added_ = false;
}
}
void WinTray::RemoveTrayIcon() { Shell_NotifyIcon(NIM_DELETE, &nid_); }
void WinTray::ShowApplicationWindow() {
if (show_window_) {
@@ -33,7 +33,6 @@ class WinTray {
bool HandleTrayMessage(MSG* msg);
private:
bool EnsureTrayIcon();
void ShowApplicationWindow();
void OpenSettings();
@@ -43,7 +42,6 @@ class WinTray {
std::wstring tip_;
int language_index_;
NOTIFYICONDATA nid_;
bool tray_icon_added_ = false;
std::function<void()> show_window_;
std::function<void()> hide_window_;
std::function<void()> open_settings_;
@@ -384,7 +384,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) {
const std::string stream_id = MakeDisplayStreamId(idx);
callback_(nv12_frame_, nv12_size, even_width, even_height,
stream_id.c_str(), nullptr);
stream_id.c_str());
} else {
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size());
@@ -212,8 +212,7 @@ void ScreenCapturerGdi::CaptureLoop() {
if (callback_) {
const std::string stream_id = MakeDisplayStreamId(idx);
callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str(),
nullptr);
callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str());
}
SelectObject(mem_dc, old);
@@ -406,7 +406,7 @@ void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
const std::string stream_id = MakeDisplayStreamId(id);
on_data_(nv12_frame_, nv12_size, even_width, even_height,
stream_id.c_str(), nullptr);
stream_id.c_str());
}
}
@@ -15,14 +15,10 @@
#include <vector>
#include <display_stream_id.h>
#include "captured_nv12_frame.h"
#include "interactive_state.h"
#include "named_pipe_deadline.h"
#include "rd_log.h"
#include "screen_capturer_dxgi.h"
#include "screen_capturer_gdi.h"
#include "secure_desktop_frame_schedule.h"
#include "secure_desktop_status_poller.h"
#include "service_host.h"
#include "session_helper_shared.h"
#include "wgc_plugin_api.h"
@@ -44,6 +40,17 @@ constexpr int kSecureDesktopCaptureMinFps = 30;
constexpr int kSecureDesktopCaptureMaxIntervalMs =
1000 / kSecureDesktopCaptureMinFps;
struct SecureDesktopServiceStatus {
bool service_available = false;
bool capture_active = false;
bool helper_running = false;
DWORD active_session_id = 0xFFFFFFFF;
DWORD error_code = 0;
std::string interactive_stage;
std::string interactive_desktop;
std::string error;
};
class WgcPluginCapturer final : public ScreenCapturer {
public:
using CreateFn = ScreenCapturer* (*)();
@@ -178,6 +185,41 @@ bool IsTransientWindowsServiceStatusError(const std::string& error) {
error == "pipe_read_failed";
}
bool ReadPipeMessage(HANDLE pipe, std::vector<uint8_t>* response_out,
DWORD* error_code_out = nullptr) {
if (response_out == nullptr) {
return false;
}
response_out->clear();
if (error_code_out != nullptr) {
*error_code_out = 0;
}
std::vector<uint8_t> chunk(64 * 1024);
while (true) {
DWORD bytes_read = 0;
if (ReadFile(pipe, chunk.data(), static_cast<DWORD>(chunk.size()),
&bytes_read, nullptr)) {
response_out->insert(response_out->end(), chunk.begin(),
chunk.begin() + bytes_read);
return true;
}
const DWORD error = GetLastError();
response_out->insert(response_out->end(), chunk.begin(),
chunk.begin() + bytes_read);
if (error == ERROR_MORE_DATA) {
continue;
}
if (error_code_out != nullptr) {
*error_code_out = error;
}
return false;
}
}
bool ParseSecureDesktopFrameResponse(const std::vector<uint8_t>& response,
std::vector<uint8_t>* nv12_frame_out,
int* width_out, int* height_out,
@@ -218,27 +260,25 @@ bool ParseSecureDesktopFrameResponse(const std::vector<uint8_t>& response,
return true;
}
SecureDesktopServiceStatus QuerySecureDesktopServiceStatus() {
SecureDesktopServiceStatus status;
std::vector<uint8_t> response;
DWORD error_code = 0;
if (!QueryNamedPipeWithDeadline(kCrossDeskServicePipeName, "status",
kSecureDesktopStatusPipeTimeoutMs, &response,
&status.error, &error_code)) {
status.error_code = error_code;
return status;
}
Json json = Json::parse(response.begin(), response.end(), nullptr, false);
if (json.is_discarded() || !json.is_object()) {
status.error = "invalid_service_status_json";
return status;
bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) {
if (status == nullptr) {
return false;
}
status.service_available = json.value("ok", false);
if (!status.service_available) {
status.error = json.value("error", std::string("service_unavailable"));
status.error_code = json.value("code", 0u);
return status;
*status = {};
const std::string response =
QueryCrossDeskService("status", kSecureDesktopStatusPipeTimeoutMs);
Json json = Json::parse(response, nullptr, false);
if (json.is_discarded() || !json.is_object()) {
status->error = "invalid_service_status_json";
return false;
}
status->service_available = json.value("ok", false);
if (!status->service_available) {
status->error = json.value("error", std::string("service_unavailable"));
status->error_code = json.value("code", 0u);
return true;
}
if (ShouldNormalizeUnlockToUserDesktop(
@@ -252,27 +292,26 @@ SecureDesktopServiceStatus QuerySecureDesktopServiceStatus() {
json.value("password_box_visible", false),
json.value("unlock_ui_visible", false),
json.value("last_session_event", std::string()))) {
status.active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
status.interactive_stage = "user-desktop";
status.interactive_desktop.clear();
status.capture_active = false;
return status;
status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
status->interactive_stage = "user-desktop";
status->interactive_desktop.clear();
status->capture_active = false;
return true;
}
status.active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
status.helper_running = json.value("secure_input_helper_running", false);
status.helper_process_id = json.value("secure_input_helper_pid", 0u);
status.interactive_stage = json.value("interactive_stage", std::string());
status.interactive_desktop =
status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
status->helper_running = json.value("secure_input_helper_running", false);
status->interactive_stage = json.value("interactive_stage", std::string());
status->interactive_desktop =
json.value("interactive_input_desktop", std::string());
const bool secure_desktop_active =
json.value("interactive_secure_desktop_active",
json.value("secure_desktop_active", false));
status.capture_active =
status.active_session_id != 0xFFFFFFFF &&
status->capture_active =
status->active_session_id != 0xFFFFFFFF &&
(secure_desktop_active ||
IsSecureDesktopInteractionRequired(status.interactive_stage));
return status;
IsSecureDesktopInteractionRequired(status->interactive_stage));
return true;
}
bool QuerySecureDesktopHelperCommand(DWORD session_id,
@@ -283,29 +322,60 @@ bool QuerySecureDesktopHelperCommand(DWORD session_id,
return false;
}
response_out->clear();
const std::wstring pipe_name =
GetCrossDeskSecureInputHelperPipeName(session_id);
std::string error;
DWORD code = 0;
// The legacy single-frame path includes a full GDI capture and a large
// response. Give it a separate deadline from the small control messages.
const DWORD timeout_ms =
command.rfind(kCrossDeskSecureInputCaptureCommandPrefix, 0) == 0
? 1000
: kSecureDesktopHelperPipeTimeoutMs;
const bool ok = QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms,
response_out, &error, &code);
if (!ok && error_out != nullptr) {
*error_out = error + ":" + std::to_string(code);
if (!WaitNamedPipeW(pipe_name.c_str(), kSecureDesktopHelperPipeTimeoutMs)) {
if (error_out != nullptr) {
*error_out = "pipe_unavailable:" + std::to_string(GetLastError());
}
return false;
}
return ok;
HANDLE pipe = CreateFileW(pipe_name.c_str(), GENERIC_READ | GENERIC_WRITE, 0,
nullptr, OPEN_EXISTING, 0, nullptr);
if (pipe == INVALID_HANDLE_VALUE) {
if (error_out != nullptr) {
*error_out = "pipe_connect_failed:" + std::to_string(GetLastError());
}
return false;
}
DWORD pipe_mode = PIPE_READMODE_MESSAGE;
SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr);
DWORD bytes_written = 0;
if (!WriteFile(pipe, command.data(), static_cast<DWORD>(command.size()),
&bytes_written, nullptr)) {
const DWORD error = GetLastError();
CloseHandle(pipe);
if (error_out != nullptr) {
*error_out = "pipe_write_failed:" + std::to_string(error);
}
return false;
}
DWORD read_error = 0;
const bool read_ok = ReadPipeMessage(pipe, response_out, &read_error);
CloseHandle(pipe);
if (!read_ok) {
if (error_out != nullptr) {
*error_out = "pipe_read_failed:" + std::to_string(read_error);
}
return false;
}
return true;
}
bool QuerySecureDesktopHelperFrame(
DWORD session_id, int left, int top, int width, int height,
bool show_cursor, const std::string& stage, const std::string& desktop,
std::vector<uint8_t>* nv12_frame_out, int* captured_width_out,
int* captured_height_out, std::string* error_out) {
bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
int width, int height, bool show_cursor,
const std::string& stage,
const std::string& desktop,
std::vector<uint8_t>* nv12_frame_out,
int* captured_width_out,
int* captured_height_out,
std::string* error_out) {
if (nv12_frame_out == nullptr || captured_width_out == nullptr ||
captured_height_out == nullptr) {
return false;
@@ -333,17 +403,8 @@ ScreenCapturerWin::~ScreenCapturerWin() { Destroy(); }
int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
cb_orig_ = cb;
native_output_logged_.store(false, std::memory_order_relaxed);
native_output_error_logged_.store(false, std::memory_order_relaxed);
try {
native_frame_pool_ = CapturedNv12FramePool::Create();
} catch (...) {
LOG_ERROR("Windows capturer: failed to create native NV12 frame pool");
return -1;
}
cb_ = [this](unsigned char* data, int size, int w, int h,
const char* reported_stream_id,
const MiniRtcNativeVideoFrame* native_frame) {
const char* reported_stream_id) {
if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
return;
}
@@ -389,8 +450,7 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
"elapsed_ms={}",
raw_stream_id, mapped_stream_id, w, h, size, elapsed_ms);
}
EmitCapturedFrame(data, size, w, h, mapped_stream_id.c_str(),
native_frame);
if (cb_orig_) cb_orig_(data, size, w, h, mapped_stream_id.c_str());
};
int ret = -1;
@@ -453,52 +513,9 @@ int ScreenCapturerWin::Destroy() {
stream_id_alias_.clear();
handle_to_canonical_index_.clear();
}
native_frame_pool_.reset();
return 0;
}
void ScreenCapturerWin::EmitCapturedFrame(
unsigned char* data, int size, int width, int height,
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame) {
if (!cb_orig_) {
return;
}
if (native_frame) {
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
LOG_INFO("Windows capturer native frame output enabled (type={})",
static_cast<uint32_t>(native_frame->type));
}
cb_orig_(data, size, width, height, stream_id, native_frame);
return;
}
CapturedNv12Frame* owned_frame = nullptr;
if (native_frame_pool_ && data && size > 0 && width > 0 && height > 0) {
owned_frame = native_frame_pool_->CopyFrom(
data, static_cast<size_t>(size), static_cast<uint32_t>(width),
static_cast<uint32_t>(height));
}
if (!owned_frame) {
if (!native_output_error_logged_.exchange(true,
std::memory_order_relaxed)) {
LOG_WARN(
"Windows capturer could not retain a native NV12 frame; falling "
"back to copied CPU input (size={}x{}, bytes={})",
width, height, size);
}
cb_orig_(data, size, width, height, stream_id, nullptr);
return;
}
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
LOG_INFO("Windows capturer native NV12 output enabled");
}
cb_orig_(nullptr, static_cast<int>(owned_frame->Size()), width, height,
stream_id, owned_frame->Descriptor());
owned_frame->Release();
}
int ScreenCapturerWin::Start(bool show_cursor) {
if (!impl_) return -1;
if (running_.load(std::memory_order_relaxed)) {
@@ -1091,35 +1108,17 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
const int frame_interval_ms =
fps_ > 0 ? (std::min)(kSecureDesktopCaptureMaxIntervalMs, 1000 / fps_)
: kSecureDesktopCaptureMaxIntervalMs;
ULONGLONG last_status_tick = 0;
ULONGLONG last_error_tick = 0;
bool last_capture_active = false;
bool last_service_available = true;
std::string last_stage;
std::string last_service_error;
ULONGLONG capture_stage_started_tick = 0;
bool post_secure_restart_pending = false;
ULONGLONG post_secure_restart_deadline_tick = 0;
ULONGLONG last_post_secure_restart_tick = 0;
SecureDesktopServiceStatus status;
SecureDesktopStatusPoller status_poller(
QuerySecureDesktopServiceStatus,
std::chrono::milliseconds(kSecureDesktopStatusIntervalMs));
SecureDesktopFrameSchedule frame_schedule;
frame_schedule.Reset(GetTickCount64());
ULONGLONG stats_started = GetTickCount64();
unsigned shared_frames = 0, fallback_frames = 0, shared_waits = 0;
unsigned shared_restarts = 0;
int64_t max_status_ms = 0;
auto report_stats = [&](bool force) {
const ULONGLONG elapsed = GetTickCount64() - stats_started;
if (elapsed == 0 || (!force && elapsed < 5000)) return;
if (shared_frames || fallback_frames || shared_waits || shared_restarts) {
LOG_INFO(
"Secure capture delivery: shared_fps={:.1f}, fallback_fps={:.1f}, "
"pending_waits={}, shared_restarts={}, status_max_ms={}",
shared_frames * 1000.0 / elapsed, fallback_frames * 1000.0 / elapsed,
shared_waits, shared_restarts, max_status_ms);
}
stats_started = GetTickCount64();
shared_frames = fallback_frames = shared_waits = shared_restarts = 0;
max_status_ms = 0;
};
std::vector<uint8_t> secure_frame;
while (running_.load(std::memory_order_relaxed)) {
@@ -1129,60 +1128,67 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
}
const ULONGLONG now = GetTickCount64();
const auto frame_started = std::chrono::steady_clock::now();
report_stats(false);
if (auto sample = status_poller.Take()) {
const auto previous = std::exchange(status, std::move(sample->status));
max_status_ms = (std::max)(max_status_ms, sample->query_ms);
if (status.service_available != previous.service_available ||
status.error != previous.error) {
if (status.service_available) {
LOG_INFO(
"Windows capturer secure desktop service available, "
"polling session_id={}",
status.active_session_id);
} else if (IsTransientWindowsServiceStatusError(status.error)) {
LOG_INFO(
"Windows capturer secure desktop service temporarily "
"unavailable; "
"keeping last capture state: error={}, code={}",
status.error, status.error_code);
} else {
LOG_WARN(
"Windows capturer secure desktop service unavailable: "
"error={}, code={}",
status.error, status.error_code);
if (last_status_tick == 0 ||
now - last_status_tick >= kSecureDesktopStatusIntervalMs) {
SecureDesktopServiceStatus latest_status;
const bool status_ok = QuerySecureDesktopServiceStatus(&latest_status);
status = latest_status;
if (status_ok) {
const bool service_changed =
status.service_available != last_service_available;
const bool service_error_changed =
!status.service_available && status.error != last_service_error;
if (service_changed || service_error_changed) {
if (status.service_available) {
LOG_INFO(
"Windows capturer secure desktop service available, polling "
"session_id={}",
status.active_session_id);
} else if (IsTransientWindowsServiceStatusError(status.error)) {
LOG_INFO(
"Windows capturer secure desktop service temporarily unavailable: "
"error={}, code={}",
status.error, status.error_code);
} else {
LOG_WARN(
"Windows capturer secure desktop service unavailable: "
"error={}, code={}",
status.error, status.error_code);
}
last_service_available = status.service_available;
last_service_error = status.error;
}
} else if (last_service_available ||
last_service_error != "invalid_service_status_json") {
LOG_WARN("Windows capturer secure desktop service status query failed");
last_service_available = false;
last_service_error = "invalid_service_status_json";
}
secure_desktop_capture_active_.store(status.capture_active,
std::memory_order_relaxed);
if (status.capture_active != previous.capture_active ||
status.interactive_stage != previous.interactive_stage ||
status.active_session_id != previous.active_session_id ||
status.helper_process_id != previous.helper_process_id) {
report_stats(true);
// A restarted helper can leave our mapping/event handles alive, but
// their old producer is gone. Explicitly establish a fresh stream.
StopSecureDesktopSharedCapture(secure_shared_session_id_);
frame_schedule.Reset(now);
if (status.capture_active != last_capture_active ||
status.interactive_stage != last_stage) {
const bool secure_capture_started =
!previous.capture_active && status.capture_active;
!last_capture_active && status.capture_active;
const bool secure_capture_ended =
previous.capture_active && !status.capture_active;
last_capture_active && !status.capture_active;
capture_stage_started_tick = now;
LOG_INFO(
"Windows capturer secure desktop state: active={}, stage='{}', "
"session_id={}",
status.capture_active, status.interactive_stage,
status.active_session_id);
last_capture_active = status.capture_active;
last_stage = status.interactive_stage;
if (secure_capture_started) {
post_secure_restart_pending = false;
post_secure_desktop_waiting_for_frame_.store(
false, std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false,
std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(
false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(
0, std::memory_order_relaxed);
} else if (secure_capture_ended) {
post_secure_restart_pending = true;
post_secure_restart_deadline_tick =
@@ -1190,12 +1196,13 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
last_post_secure_restart_tick = 0;
post_secure_desktop_waiting_for_frame_.store(
true, std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false,
std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(now,
std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(
false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(
now, std::memory_order_relaxed);
}
}
last_status_tick = now;
}
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
@@ -1241,64 +1248,41 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
std::string error_message;
bool frame_delivered = false;
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
const int shared_fps = fps_ > 0
? (std::max)(kSecureDesktopCaptureMinFps, fps_)
: kSecureDesktopCaptureMinFps;
const int shared_fps =
fps_ > 0 ? (std::max)(kSecureDesktopCaptureMinFps, fps_)
: kSecureDesktopCaptureMinFps;
if (secure_shared_capture_started_ &&
frame_schedule.SharedCaptureStalled(now)) {
LOG_WARN("Secure shared capture stalled; restarting producer");
StopSecureDesktopSharedCapture(secure_shared_session_id_);
frame_schedule.Reset(GetTickCount64());
++shared_restarts;
}
bool shared_ready = false;
const bool was_started = secure_shared_capture_started_;
if (was_started || frame_schedule.CanStart(now)) {
shared_ready = StartSecureDesktopSharedCapture(
status.active_session_id, left, top, width, height,
status.interactive_stage, status.interactive_desktop, show_cursor,
shared_fps, &error_message);
if (!shared_ready) {
frame_schedule.OnStartFailure(GetTickCount64());
} else if (!was_started) {
frame_schedule.Reset(GetTickCount64());
}
}
if (shared_ready &&
if (StartSecureDesktopSharedCapture(status.active_session_id, left, top,
width, height,
status.interactive_stage,
status.interactive_desktop,
show_cursor, shared_fps,
&error_message) &&
ReadSecureDesktopSharedFrame(
static_cast<DWORD>((std::max)(50, frame_interval_ms + 20)),
&secure_frame, &captured_width, &captured_height, &error_message)) {
frame_delivered = true;
++shared_frames;
frame_schedule.OnSharedFrame(GetTickCount64());
}
const bool frame_pending = shared_ready && !frame_delivered &&
IsPendingSecureDesktopFrame(error_message);
if (frame_pending) ++shared_waits;
if (shared_ready && !frame_delivered && !frame_pending) {
StopSecureDesktopSharedCapture(secure_shared_session_id_);
frame_schedule.OnStartFailure(GetTickCount64());
}
if (!frame_delivered && !frame_pending &&
QuerySecureDesktopHelperFrame(
status.active_session_id, left, top, width, height, show_cursor,
status.interactive_stage, status.interactive_desktop, &secure_frame,
static_cast<DWORD>(frame_interval_ms + 20), &secure_frame,
&captured_width, &captured_height, &error_message)) {
if (cb_orig_ && !secure_frame.empty()) {
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
captured_width, captured_height, display_name.c_str());
}
frame_delivered = true;
++fallback_frames;
}
if (frame_delivered && !secure_frame.empty()) {
EmitCapturedFrame(secure_frame.data(),
static_cast<int>(secure_frame.size()), captured_width,
captured_height, display_name.c_str());
if (!frame_delivered &&
QuerySecureDesktopHelperFrame(status.active_session_id, left, top,
width, height, show_cursor,
status.interactive_stage,
status.interactive_desktop,
&secure_frame, &captured_width,
&captured_height, &error_message)) {
if (cb_orig_ && !secure_frame.empty()) {
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
captured_width, captured_height, display_name.c_str());
}
frame_delivered = true;
}
if (!frame_delivered && !frame_pending) {
if (!frame_delivered) {
const bool transient_error =
IsTransientSecureDesktopFrameError(error_message);
const bool in_grace_period = capture_stage_started_tick != 0 &&
@@ -1329,18 +1313,9 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
}
}
const auto elapsed_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - frame_started)
.count();
const int sleep_ms = SecureDesktopFrameSchedule::RemainingFrameDelay(
frame_interval_ms, static_cast<uint64_t>(elapsed_ms));
if (sleep_ms > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
}
std::this_thread::sleep_for(std::chrono::milliseconds(frame_interval_ms));
}
report_stats(true);
StopSecureDesktopSharedCapture(secure_shared_session_id_);
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
}
@@ -14,7 +14,6 @@
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
@@ -23,8 +22,6 @@
namespace crossdesk {
class CapturedNv12FramePool;
class ScreenCapturerWin : public ScreenCapturer {
public:
ScreenCapturerWin();
@@ -56,8 +53,6 @@ class ScreenCapturerWin : public ScreenCapturer {
std::mutex alias_mutex_;
std::vector<DisplayInfo> canonical_displays_;
std::atomic<bool> invalid_stream_id_logged_{false};
std::atomic<bool> native_output_logged_{false};
std::atomic<bool> native_output_error_logged_{false};
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<bool> show_cursor_{true};
@@ -82,13 +77,9 @@ class ScreenCapturerWin : public ScreenCapturer {
std::string secure_shared_stage_;
std::string secure_shared_desktop_;
bool secure_shared_capture_started_ = false;
std::shared_ptr<CapturedNv12FramePool> native_frame_pool_;
void BuildCanonicalFromImpl();
void RebuildAliasesFromImpl();
void EmitCapturedFrame(unsigned char* data, int size, int width, int height,
const char* stream_id,
const MiniRtcNativeVideoFrame* native_frame = nullptr);
void StopSecureCaptureThread();
bool RestartCaptureBackendAfterSecureDesktop();
void SecureDesktopCaptureLoop();
@@ -1,50 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SECURE_DESKTOP_FRAME_SCHEDULE_H_
#define _SECURE_DESKTOP_FRAME_SCHEDULE_H_
#include <cstdint>
#include <string>
namespace crossdesk {
inline bool IsPendingSecureDesktopFrame(const std::string& error) {
return error == "frame_wait_timeout" ||
error == "shared_frame_write_in_progress" ||
error == "shared_frame_changed_during_read";
}
// A 4K GDI frame can take longer than a single consumer wait. Do not issue
// another expensive screenshot while the shared producer is still healthy.
class SecureDesktopFrameSchedule {
public:
void Reset(uint64_t now) {
last_shared_frame_ = now;
next_start_attempt_ = 0;
}
void OnSharedFrame(uint64_t now) { last_shared_frame_ = now; }
bool SharedCaptureStalled(uint64_t now) const {
return now - last_shared_frame_ >= 1000;
}
bool CanStart(uint64_t now) const { return now >= next_start_attempt_; }
void OnStartFailure(uint64_t now) { next_start_attempt_ = now + 1000; }
static int RemainingFrameDelay(int interval_ms, uint64_t elapsed_ms) {
return elapsed_ms >= static_cast<uint64_t>(interval_ms)
? 0
: interval_ms - static_cast<int>(elapsed_ms);
}
private:
uint64_t last_shared_frame_ = 0;
uint64_t next_start_attempt_ = 0;
};
} // namespace crossdesk
#endif
@@ -1,110 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SECURE_DESKTOP_STATUS_POLLER_H_
#define _SECURE_DESKTOP_STATUS_POLLER_H_
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <functional>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <utility>
namespace crossdesk {
struct SecureDesktopServiceStatus {
bool service_available = false;
bool capture_active = false;
bool helper_running = false;
uint32_t helper_process_id = 0;
uint32_t active_session_id = 0xFFFFFFFF;
uint32_t error_code = 0;
std::string interactive_stage;
std::string interactive_desktop;
std::string error;
void UpdateFrom(SecureDesktopServiceStatus latest) {
if (latest.service_available) {
*this = std::move(latest);
} else {
// Failed IPC says nothing about the desktop. Retain the last confirmed
// capture state until a valid response reports a change or an unlock.
service_available = false;
error = std::move(latest.error);
error_code = latest.error_code;
}
}
};
// The query runs without holding the snapshot mutex. Slow service/helper IPC
// must not stop delivery of frames that are already available in shared memory.
class SecureDesktopStatusPoller {
public:
using Query = std::function<SecureDesktopServiceStatus()>;
struct Sample {
SecureDesktopServiceStatus status;
int64_t query_ms = 0;
};
explicit SecureDesktopStatusPoller(Query query,
std::chrono::milliseconds interval)
: thread_([this, query = std::move(query), interval]() {
std::unique_lock<std::mutex> lock(mutex_);
while (!stopping_) {
lock.unlock();
const auto started = std::chrono::steady_clock::now();
auto latest = query();
const auto elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - started);
lock.lock();
sample_.status.UpdateFrom(std::move(latest));
sample_.query_ms = elapsed.count();
pending_ = true;
// Even a failed/slow query gets a pause, avoiding a retry storm.
wake_.wait_for(lock, interval, [this] { return stopping_; });
}
}) {}
~SecureDesktopStatusPoller() {
{
std::lock_guard<std::mutex> lock(mutex_);
stopping_ = true;
}
wake_.notify_one();
thread_.join();
}
SecureDesktopStatusPoller(const SecureDesktopStatusPoller&) = delete;
SecureDesktopStatusPoller& operator=(const SecureDesktopStatusPoller&) =
delete;
std::optional<Sample> Take() {
std::lock_guard<std::mutex> lock(mutex_);
if (!pending_) {
return std::nullopt;
}
pending_ = false;
return sample_;
}
private:
std::mutex mutex_;
std::condition_variable wake_;
bool stopping_ = false;
bool pending_ = false;
Sample sample_;
std::thread thread_;
};
} // namespace crossdesk
#endif
@@ -1,101 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _INTERACTIVE_DESKTOP_H_
#define _INTERACTIVE_DESKTOP_H_
#include <Windows.h>
#include <string>
namespace crossdesk {
inline std::wstring GetDesktopNameW(HDESK desktop) {
DWORD bytes_needed = 0;
if (desktop == nullptr) {
return {};
}
GetUserObjectInformationW(desktop, UOI_NAME, nullptr, 0, &bytes_needed);
if (bytes_needed == 0) {
return {};
}
std::wstring name(bytes_needed / sizeof(wchar_t), L'\0');
if (!GetUserObjectInformationW(desktop, UOI_NAME, name.data(), bytes_needed,
&bytes_needed)) {
return {};
}
while (!name.empty() && name.back() == L'\0') {
name.pop_back();
}
return name;
}
// Use only on worker threads without windows or hooks. Restore the original
// desktop before closing the handle: Windows cannot close a desktop in use.
class ScopedInteractiveDesktop {
public:
ScopedInteractiveDesktop()
: original_(GetThreadDesktop(GetCurrentThreadId())) {}
ScopedInteractiveDesktop(const ScopedInteractiveDesktop&) = delete;
ScopedInteractiveDesktop& operator=(const ScopedInteractiveDesktop&) = delete;
~ScopedInteractiveDesktop() {
if (desktop_ != nullptr) {
SetThreadDesktop(original_);
CloseDesktop(desktop_);
}
}
bool Bind(const std::wstring& fallback_name) {
constexpr ACCESS_MASK access =
DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS |
DESKTOP_SWITCHDESKTOP | DESKTOP_JOURNALPLAYBACK;
// SendInput requires playback access on the attached desktop; otherwise
// it returns ERROR_ACCESS_DENIED even for a correctly attached SYSTEM helper.
// The service's stage/name can lag a lock, unlock, or UAC transition.
// Opening a cached desktop can succeed even when it is no longer visible.
HDESK desktop = OpenInputDesktop(0, FALSE, access);
if (desktop == nullptr && !fallback_name.empty()) {
desktop = OpenDesktopW(fallback_name.c_str(), 0, FALSE, access);
}
if (desktop == nullptr) {
return false;
}
if (!SetThreadDesktop(desktop)) {
const DWORD error = GetLastError();
CloseDesktop(desktop);
SetLastError(error);
return false;
}
if (desktop_ != nullptr) {
CloseDesktop(desktop_);
}
desktop_ = desktop;
name_ = GetDesktopNameW(desktop_);
return true;
}
bool IsInputDesktop() const {
HDESK input = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS);
if (input == nullptr) {
return false;
}
const std::wstring input_name = GetDesktopNameW(input);
CloseDesktop(input);
return !name_.empty() && _wcsicmp(name_.c_str(), input_name.c_str()) == 0;
}
const std::wstring& name() const { return name_; }
private:
HDESK original_ = nullptr;
HDESK desktop_ = nullptr;
std::wstring name_;
};
} // namespace crossdesk
#endif
@@ -1,143 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _NAMED_PIPE_DEADLINE_H_
#define _NAMED_PIPE_DEADLINE_H_
#include <Windows.h>
#include <algorithm>
#include <cstdint>
#include <string>
#include <vector>
namespace crossdesk {
namespace pipe_deadline_detail {
struct Handle {
explicit Handle(HANDLE handle = INVALID_HANDLE_VALUE) : value(handle) {}
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
HANDLE value = INVALID_HANDLE_VALUE;
~Handle() {
if (value != INVALID_HANDLE_VALUE && value != nullptr) CloseHandle(value);
}
};
inline DWORD Remaining(ULONGLONG deadline) {
const ULONGLONG now = GetTickCount64();
return now >= deadline ? 0 : static_cast<DWORD>(deadline - now);
}
inline bool Transfer(HANDLE pipe, HANDLE event, bool write, void* data,
DWORD size, ULONGLONG deadline, DWORD* transferred,
DWORD* error) {
*transferred = 0;
if (Remaining(deadline) == 0) {
*error = ERROR_SEM_TIMEOUT;
return false;
}
ResetEvent(event);
OVERLAPPED operation{};
operation.hEvent = event;
const BOOL complete =
write ? WriteFile(pipe, data, size, transferred, &operation)
: ReadFile(pipe, data, size, transferred, &operation);
*error = complete ? ERROR_SUCCESS : GetLastError();
if (*error == ERROR_IO_PENDING) {
const DWORD wait = WaitForSingleObject(event, Remaining(deadline));
if (wait != WAIT_OBJECT_0) {
*error = wait == WAIT_TIMEOUT ? ERROR_SEM_TIMEOUT : GetLastError();
// Keep the OVERLAPPED and buffer alive until cancellation completes.
CancelIoEx(pipe, &operation);
DWORD ignored = 0;
GetOverlappedResult(pipe, &operation, &ignored, TRUE);
return false;
}
} else if (*error != ERROR_SUCCESS && *error != ERROR_MORE_DATA) {
return false;
}
// Immediate MORE_DATA also needs GetOverlappedResult: ReadFile may leave
// its byte-count output at zero even though part of the message was read.
const BOOL result = GetOverlappedResult(pipe, &operation, transferred, FALSE);
*error = result ? ERROR_SUCCESS : GetLastError();
return result != FALSE;
}
} // namespace pipe_deadline_detail
// One deadline covers connection, write, and every response chunk. A connect
// timeout alone does not bound a synchronous ReadFile on an unresponsive peer.
inline bool QueryNamedPipeWithDeadline(const std::wstring& name,
const std::string& command,
DWORD timeout_ms,
std::vector<uint8_t>* response,
std::string* error, DWORD* error_code) {
using namespace pipe_deadline_detail;
response->clear();
error->clear();
*error_code = ERROR_SUCCESS;
const ULONGLONG deadline = GetTickCount64() + timeout_ms;
Handle pipe;
while (true) {
const DWORD remaining = Remaining(deadline);
if (remaining == 0) break;
if (WaitNamedPipeW(name.c_str(), remaining)) {
pipe.value =
CreateFileW(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr,
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
if (pipe.value != INVALID_HANDLE_VALUE) break;
}
*error_code = GetLastError();
if (*error_code != ERROR_FILE_NOT_FOUND && *error_code != ERROR_PIPE_BUSY &&
*error_code != ERROR_SEM_TIMEOUT)
break;
Sleep((std::min)(DWORD{10}, Remaining(deadline)));
}
if (pipe.value == INVALID_HANDLE_VALUE) {
*error = "pipe_unavailable";
if (*error_code == ERROR_SUCCESS) *error_code = ERROR_SEM_TIMEOUT;
return false;
}
DWORD mode = PIPE_READMODE_MESSAGE;
if (!SetNamedPipeHandleState(pipe.value, &mode, nullptr, nullptr)) {
*error = "pipe_connect_failed";
*error_code = GetLastError();
return false;
}
Handle event{CreateEventW(nullptr, TRUE, FALSE, nullptr)};
if (event.value == nullptr) {
*error = "pipe_connect_failed";
*error_code = GetLastError();
return false;
}
DWORD transferred = 0;
if (!Transfer(pipe.value, event.value, true,
const_cast<char*>(command.data()),
static_cast<DWORD>(command.size()), deadline, &transferred,
error_code) ||
transferred != command.size()) {
*error = "pipe_write_failed";
return false;
}
std::vector<uint8_t> chunk(64 * 1024);
while (true) {
const bool complete = Transfer(pipe.value, event.value, false, chunk.data(),
static_cast<DWORD>(chunk.size()), deadline,
&transferred, error_code);
if (!complete && *error_code != ERROR_MORE_DATA) {
response->clear();
*error = "pipe_read_failed";
return false;
}
response->insert(response->end(), chunk.begin(),
chunk.begin() + transferred);
if (complete) return true;
}
}
} // namespace crossdesk
#endif
@@ -16,7 +16,6 @@
#include <string>
#include <thread>
#include "interactive_state.h"
#include "path_manager.h"
#include "rd_log.h"
#include "session_helper_shared.h"
@@ -1460,18 +1459,18 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
std::string CrossDeskServiceHost::ResolveInteractiveDesktopLocked(
const std::string& interactive_stage) const {
if (interactive_stage == "lock-screen") {
return "Default";
}
if (session_helper_status_ok_ &&
session_helper_report_input_desktop_available_ &&
!session_helper_report_input_desktop_.empty() &&
(IsSecureDesktopInteractionRequired(interactive_stage) ||
(interactive_stage == "credential-ui" ||
session_helper_report_consent_ui_visible_)) {
return session_helper_report_input_desktop_;
}
if (interactive_stage == "lock-screen") {
return "Default";
}
if (input_desktop_available_ && !input_desktop_name_.empty() &&
(interactive_stage == "credential-ui" || consent_ui_visible_)) {
return input_desktop_name_;
@@ -21,7 +21,6 @@
#include <utility>
#include <vector>
#include "interactive_desktop.h"
#include "path_manager.h"
#include "rd_log.h"
#include "session_helper_shared.h"
@@ -31,7 +30,6 @@ namespace {
using crossdesk::get_logger;
using crossdesk::InitLogger;
using crossdesk::ScopedInteractiveDesktop;
using Json = nlohmann::json;
constexpr DWORD kSessionHelperStatePollMs = 1000;
@@ -540,8 +538,123 @@ void HelperIpcServerLoop(HANDLE stop_event, DWORD session_id,
}
}
std::wstring GetDesktopNameW(HDESK desktop) {
if (desktop == nullptr) {
return L"";
}
DWORD bytes_needed = 0;
GetUserObjectInformationW(desktop, UOI_NAME, nullptr, 0, &bytes_needed);
if (bytes_needed == 0) {
return L"";
}
std::wstring desktop_name(bytes_needed / sizeof(wchar_t), L'\0');
if (!GetUserObjectInformationW(desktop, UOI_NAME, desktop_name.data(),
bytes_needed, &bytes_needed)) {
return L"";
}
while (!desktop_name.empty() && desktop_name.back() == L'\0') {
desktop_name.pop_back();
}
return desktop_name;
}
std::wstring GetCurrentThreadDesktopNameW() {
return crossdesk::GetDesktopNameW(GetThreadDesktop(GetCurrentThreadId()));
return GetDesktopNameW(GetThreadDesktop(GetCurrentThreadId()));
}
constexpr ACCESS_MASK kCrossDeskInteractiveDesktopAccess =
DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS |
DESKTOP_SWITCHDESKTOP;
bool EnsureThreadDesktop(const wchar_t* desktop_name,
HDESK* opened_desktop_out = nullptr) {
if (desktop_name == nullptr) {
return false;
}
std::wstring current_desktop = GetCurrentThreadDesktopNameW();
if (!current_desktop.empty() &&
_wcsicmp(current_desktop.c_str(), desktop_name) == 0) {
return true;
}
HDESK desktop =
OpenDesktopW(desktop_name, 0, FALSE, kCrossDeskInteractiveDesktopAccess);
if (desktop == nullptr) {
return false;
}
if (!SetThreadDesktop(desktop)) {
CloseDesktop(desktop);
return false;
}
if (opened_desktop_out != nullptr) {
*opened_desktop_out = desktop;
} else {
CloseDesktop(desktop);
}
return true;
}
bool EnsureThreadInputDesktop(HDESK* opened_desktop_out = nullptr) {
HDESK desktop =
OpenInputDesktop(0, FALSE, kCrossDeskInteractiveDesktopAccess);
if (desktop == nullptr) {
return false;
}
const std::wstring input_desktop = GetDesktopNameW(desktop);
const std::wstring current_desktop = GetCurrentThreadDesktopNameW();
if (!input_desktop.empty() && !current_desktop.empty() &&
_wcsicmp(input_desktop.c_str(), current_desktop.c_str()) == 0) {
if (opened_desktop_out != nullptr) {
*opened_desktop_out = desktop;
} else {
CloseDesktop(desktop);
}
return true;
}
if (!SetThreadDesktop(desktop)) {
CloseDesktop(desktop);
return false;
}
if (opened_desktop_out != nullptr) {
*opened_desktop_out = desktop;
} else {
CloseDesktop(desktop);
}
return true;
}
bool EnsureThreadInteractiveDesktop(HDESK* opened_desktop_out = nullptr) {
if (opened_desktop_out != nullptr) {
*opened_desktop_out = nullptr;
}
if (EnsureThreadInputDesktop(opened_desktop_out)) {
return true;
}
const DWORD input_desktop_error = GetLastError();
if (EnsureThreadDesktop(L"Winlogon", opened_desktop_out)) {
return true;
}
const DWORD winlogon_error = GetLastError();
LOG_WARN(
"Failed to switch secure input helper desktop, input_error={}, "
"winlogon_error={}, current='{}'",
input_desktop_error, winlogon_error,
WideToUtf8(GetCurrentThreadDesktopNameW()));
SetLastError(winlogon_error != ERROR_SUCCESS ? winlogon_error
: input_desktop_error);
return false;
}
std::wstring DesktopNameForInteractiveStage(
@@ -587,10 +700,8 @@ DesktopSwitchDetails BuildDesktopSwitchDetails(
return details;
}
InputInjectionResult BuildInputSuccess(DesktopSwitchDetails desktop) {
InputInjectionResult result;
result.desktop = std::move(desktop);
return result;
InputInjectionResult BuildInputSuccess() {
return {};
}
InputInjectionResult BuildInputFailure(const char* error, DWORD error_code,
@@ -621,23 +732,48 @@ Json BuildInputFailureJson(const InputInjectionResult& result) {
bool EnsureThreadInteractiveDesktopForStage(
const std::string& interactive_stage,
const std::string& interactive_desktop,
ScopedInteractiveDesktop* desktop,
HDESK* opened_desktop_out = nullptr,
DesktopSwitchDetails* switch_details = nullptr) {
if (opened_desktop_out != nullptr) {
*opened_desktop_out = nullptr;
}
DesktopSwitchDetails local_details =
BuildDesktopSwitchDetails(interactive_stage, interactive_desktop);
if (switch_details != nullptr) {
*switch_details = local_details;
}
std::wstring desktop_name =
const std::wstring desktop_name =
DesktopNameForInteractiveStage(interactive_stage, interactive_desktop);
if (desktop_name.empty()) {
desktop_name = L"Winlogon";
}
if (desktop->Bind(desktop_name)) {
if (!desktop_name.empty()) {
if (EnsureThreadDesktop(desktop_name.c_str(), opened_desktop_out)) {
if (switch_details != nullptr) {
switch_details->current_desktop =
WideToUtf8(GetCurrentThreadDesktopNameW());
}
return true;
}
const DWORD error = GetLastError();
if (switch_details != nullptr) {
switch_details->target_desktop = WideToUtf8(desktop->name());
switch_details->current_desktop = switch_details->target_desktop;
switch_details->error_code = error;
switch_details->current_desktop =
WideToUtf8(GetCurrentThreadDesktopNameW());
}
LOG_WARN(
"Failed to switch secure input helper to stage desktop, stage='{}', "
"desktop='{}', error={}, current='{}'",
interactive_stage, WideToUtf8(desktop_name),
error,
WideToUtf8(GetCurrentThreadDesktopNameW()));
SetLastError(error);
return false;
}
if (EnsureThreadInteractiveDesktop(opened_desktop_out)) {
if (switch_details != nullptr) {
switch_details->current_desktop = WideToUtf8(GetCurrentThreadDesktopNameW());
}
return true;
}
@@ -647,15 +783,18 @@ bool EnsureThreadInteractiveDesktopForStage(
switch_details->error_code = error;
switch_details->current_desktop = WideToUtf8(GetCurrentThreadDesktopNameW());
}
LOG_WARN(
"Failed to switch secure input helper desktop, stage='{}', "
"fallback='{}', error={}, current='{}'",
interactive_stage, WideToUtf8(desktop_name), error,
WideToUtf8(GetCurrentThreadDesktopNameW()));
SetLastError(error);
return false;
}
struct ScopedDesktopHandle {
HDESK handle = nullptr;
~ScopedDesktopHandle() {
if (handle != nullptr) {
CloseDesktop(handle);
}
}
};
bool PreferSideSpecificVkInjection(int key_code) {
switch (key_code) {
case VK_LSHIFT:
@@ -676,11 +815,11 @@ InputInjectionResult InjectKeyboardInput(
int key_code, bool is_down, uint32_t scan_code, bool extended,
const std::string& interactive_stage,
const std::string& interactive_desktop) {
ScopedInteractiveDesktop desktop;
ScopedDesktopHandle desktop;
DesktopSwitchDetails desktop_switch;
if (!EnsureThreadInteractiveDesktopForStage(interactive_stage,
interactive_desktop,
&desktop,
&desktop.handle,
&desktop_switch)) {
const DWORD error = GetLastError();
return BuildInputFailure("switch_interactive_desktop_failed",
@@ -736,7 +875,7 @@ InputInjectionResult InjectKeyboardInput(
desktop_switch);
}
return BuildInputSuccess(std::move(desktop_switch));
return BuildInputSuccess();
}
void ParseInteractionTail(const std::string& tail,
@@ -1053,11 +1192,11 @@ INPUT BuildAbsoluteMouseMoveInput(int x, int y) {
}
InputInjectionResult InjectMouseInput(const SecureMouseRequest& request) {
ScopedInteractiveDesktop desktop;
ScopedDesktopHandle desktop;
DesktopSwitchDetails desktop_switch;
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
request.interactive_desktop,
&desktop,
&desktop.handle,
&desktop_switch)) {
const DWORD error = GetLastError();
return BuildInputFailure("switch_interactive_desktop_failed",
@@ -1117,91 +1256,13 @@ InputInjectionResult InjectMouseInput(const SecureMouseRequest& request) {
desktop_switch);
}
return BuildInputSuccess(std::move(desktop_switch));
return BuildInputSuccess();
}
std::vector<uint8_t> BuildTextResponseBytes(const std::string& response) {
return std::vector<uint8_t>(response.begin(), response.end());
}
// Own the desktop before its GDI objects, and release those objects before
// restoring the original desktop. Both capture paths use the same lifecycle.
struct SecureDesktopGdiResources {
ScopedInteractiveDesktop desktop;
HDC screen_dc = nullptr;
HDC mem_dc = nullptr;
HBITMAP dib = nullptr;
HGDIOBJ old_bitmap = nullptr;
void* bits = nullptr;
const char* error = nullptr;
DWORD error_code = ERROR_SUCCESS;
~SecureDesktopGdiResources() {
if (old_bitmap != nullptr && old_bitmap != HGDI_ERROR) {
SelectObject(mem_dc, old_bitmap);
}
if (dib != nullptr) DeleteObject(dib);
if (mem_dc != nullptr) DeleteDC(mem_dc);
if (screen_dc != nullptr) ReleaseDC(nullptr, screen_dc);
}
bool Fail(const char* reason) {
error = reason;
error_code = GetLastError();
if (error_code == ERROR_SUCCESS) error_code = ERROR_GEN_FAILURE;
return false;
}
bool Initialize(const SecureCaptureRequest& request) {
const auto fallback = DesktopNameForInteractiveStage(
request.interactive_stage, request.interactive_desktop);
if (!desktop.Bind(fallback.empty() ? L"Winlogon" : fallback)) {
return Fail("switch_interactive_desktop_failed");
}
screen_dc = GetDC(nullptr);
if (screen_dc == nullptr) return Fail("get_dc_failed");
mem_dc = CreateCompatibleDC(screen_dc);
if (mem_dc == nullptr) return Fail("create_dc_failed");
BITMAPINFO bmi{};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = request.width;
bmi.bmiHeader.biHeight = -request.height;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
dib = CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
if (dib == nullptr || bits == nullptr) return Fail("create_dib_failed");
old_bitmap = SelectObject(mem_dc, dib);
if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) {
return Fail("select_bitmap_failed");
}
return true;
}
bool Capture(const SecureCaptureRequest& request) {
if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc,
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
return Fail("bitblt_failed");
}
if (request.show_cursor) {
CURSORINFO cursor{};
cursor.cbSize = sizeof(cursor);
if (GetCursorInfo(&cursor) && cursor.flags == CURSOR_SHOWING &&
cursor.hCursor != nullptr) {
const int x = cursor.ptScreenPos.x - request.left;
const int y = cursor.ptScreenPos.y - request.top;
if (x >= -64 && y >= -64 && x < request.width + 64 &&
y < request.height + 64) {
DrawIconEx(mem_dc, x, y, cursor.hCursor, 0, 0, 0, nullptr, DI_NORMAL);
}
}
}
// Complete GDI writes before libyuv reads the DIB's pixel memory.
GdiFlush();
return true;
}
};
std::vector<uint8_t> CaptureSecureDesktopFrame(
const SecureCaptureRequest& request,
SecureCaptureBuffers* capture_buffers) {
@@ -1209,21 +1270,87 @@ std::vector<uint8_t> CaptureSecureDesktopFrame(
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_buffers"));
}
SecureDesktopGdiResources capture;
if (!capture.Initialize(request) || !capture.Capture(request)) {
ScopedDesktopHandle desktop;
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
request.interactive_desktop,
&desktop.handle)) {
const DWORD error = GetLastError();
return BuildTextResponseBytes(BuildErrorJson(
"switch_interactive_desktop_failed",
error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE));
}
HDC screen_dc = GetDC(nullptr);
if (screen_dc == nullptr) {
return BuildTextResponseBytes(
BuildErrorJson(capture.error, capture.error_code));
BuildErrorJson("get_dc_failed", GetLastError()));
}
HDC mem_dc = CreateCompatibleDC(screen_dc);
if (mem_dc == nullptr) {
const DWORD error = GetLastError();
ReleaseDC(nullptr, screen_dc);
return BuildTextResponseBytes(BuildErrorJson("create_dc_failed", error));
}
BITMAPINFO bmi{};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = request.width;
bmi.bmiHeader.biHeight = -request.height;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* bits = nullptr;
HBITMAP dib =
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
if (dib == nullptr || bits == nullptr) {
const DWORD error = GetLastError();
DeleteDC(mem_dc);
ReleaseDC(nullptr, screen_dc);
return BuildTextResponseBytes(BuildErrorJson("create_dib_failed", error));
}
HGDIOBJ old_bitmap = SelectObject(mem_dc, dib);
if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc,
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
const DWORD error = GetLastError();
SelectObject(mem_dc, old_bitmap);
DeleteObject(dib);
DeleteDC(mem_dc);
ReleaseDC(nullptr, screen_dc);
return BuildTextResponseBytes(BuildErrorJson("bitblt_failed", error));
}
if (request.show_cursor) {
CURSORINFO cursor_info{};
cursor_info.cbSize = sizeof(CURSORINFO);
if (GetCursorInfo(&cursor_info) && cursor_info.flags == CURSOR_SHOWING &&
cursor_info.hCursor != nullptr) {
const int cursor_x = cursor_info.ptScreenPos.x - request.left;
const int cursor_y = cursor_info.ptScreenPos.y - request.top;
if (cursor_x >= -64 && cursor_y >= -64 && cursor_x < request.width + 64 &&
cursor_y < request.height + 64) {
DrawIconEx(mem_dc, cursor_x, cursor_y, cursor_info.hCursor, 0, 0, 0,
nullptr, DI_NORMAL);
}
}
}
const size_t nv12_size =
static_cast<size_t>(request.width) * request.height * 3 / 2;
capture_buffers->nv12_frame.resize(nv12_size);
const int convert_result = libyuv::ARGBToNV12(
static_cast<const uint8_t*>(capture.bits), request.width * 4,
static_cast<const uint8_t*>(bits), request.width * 4,
capture_buffers->nv12_frame.data(), request.width,
capture_buffers->nv12_frame.data() + request.width * request.height,
request.width, request.width, request.height);
SelectObject(mem_dc, old_bitmap);
DeleteObject(dib);
DeleteDC(mem_dc);
ReleaseDC(nullptr, screen_dc);
if (convert_result != 0) {
return BuildTextResponseBytes(BuildErrorJson("argb_to_nv12_failed"));
}
@@ -1270,7 +1397,8 @@ void CloseSecureDesktopSharedCaptureResourcesLocked(
capture_state->frame_view_size = 0;
}
void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
void SecureDesktopSharedCaptureThread(
SecureSharedCaptureState* capture_state) {
if (capture_state == nullptr) {
return;
}
@@ -1293,65 +1421,79 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
static_cast<size_t>(request.width) * request.height * 3 / 2;
std::vector<uint8_t> nv12_frame(nv12_size);
std::unique_ptr<SecureDesktopGdiResources> capture;
ULONGLONG last_error_tick = 0;
auto stats_started = std::chrono::steady_clock::now();
unsigned frame_count = 0;
double capture_ms = 0, convert_ms = 0, publish_ms = 0;
auto report_stats = [&](bool force) {
const auto elapsed = std::chrono::duration<double>(
std::chrono::steady_clock::now() - stats_started)
.count();
if (elapsed <= 0 || (!force && elapsed < 5.0)) return;
if (frame_count > 0) {
LOG_INFO(
"Secure capture producer: fps={:.1f}, capture_avg_ms={:.1f}, "
"convert_avg_ms={:.1f}, publish_avg_ms={:.1f}, size={}x{}",
frame_count / elapsed, capture_ms / frame_count,
convert_ms / frame_count, publish_ms / frame_count, request.width,
request.height);
}
stats_started = std::chrono::steady_clock::now();
frame_count = 0;
capture_ms = convert_ms = publish_ms = 0;
};
ScopedDesktopHandle desktop;
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
request.interactive_desktop,
&desktop.handle)) {
LOG_ERROR("Secure shared capture desktop switch failed, error={}",
GetLastError());
return;
}
HDC screen_dc = GetDC(nullptr);
if (screen_dc == nullptr) {
LOG_ERROR("Secure shared capture GetDC failed, error={}", GetLastError());
return;
}
HDC mem_dc = CreateCompatibleDC(screen_dc);
if (mem_dc == nullptr) {
LOG_ERROR("Secure shared capture CreateCompatibleDC failed, error={}",
GetLastError());
ReleaseDC(nullptr, screen_dc);
return;
}
BITMAPINFO bmi{};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = request.width;
bmi.bmiHeader.biHeight = -request.height;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* bits = nullptr;
HBITMAP dib =
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
if (dib == nullptr || bits == nullptr) {
LOG_ERROR("Secure shared capture CreateDIBSection failed, error={}",
GetLastError());
DeleteDC(mem_dc);
ReleaseDC(nullptr, screen_dc);
return;
}
HGDIOBJ old_bitmap = SelectObject(mem_dc, dib);
while (!capture_state->stop_requested.load(std::memory_order_relaxed)) {
const auto frame_started = std::chrono::steady_clock::now();
report_stats(false);
// A lock/credential/UAC transition can change the desktop before the
// service's next status poll. Release its DCs before binding the new one.
if (capture && !capture->desktop.IsInputDesktop()) {
capture.reset();
}
if (!capture) {
capture = std::make_unique<SecureDesktopGdiResources>();
if (!capture->Initialize(request)) {
const DWORD error = capture->error_code;
capture.reset();
const ULONGLONG now = GetTickCount64();
if (last_error_tick == 0 || now - last_error_tick >= 5000) {
LOG_WARN(
"Secure shared capture initialization failed; retrying, "
"error={}",
error);
last_error_tick = now;
if (BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc,
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
if (request.show_cursor) {
CURSORINFO cursor_info{};
cursor_info.cbSize = sizeof(CURSORINFO);
if (GetCursorInfo(&cursor_info) &&
cursor_info.flags == CURSOR_SHOWING &&
cursor_info.hCursor != nullptr) {
const int cursor_x = cursor_info.ptScreenPos.x - request.left;
const int cursor_y = cursor_info.ptScreenPos.y - request.top;
if (cursor_x >= -64 && cursor_y >= -64 &&
cursor_x < request.width + 64 &&
cursor_y < request.height + 64) {
DrawIconEx(mem_dc, cursor_x, cursor_y, cursor_info.hCursor, 0, 0,
0, nullptr, DI_NORMAL);
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
continue;
}
}
if (capture->Capture(request)) {
const auto captured_at = std::chrono::steady_clock::now();
const int convert_result = libyuv::ARGBToNV12(
static_cast<const uint8_t*>(capture->bits), request.width * 4,
static_cast<const uint8_t*>(bits), request.width * 4,
nv12_frame.data(), request.width,
nv12_frame.data() + request.width * request.height, request.width,
request.width, request.height);
if (convert_result == 0) {
const auto converted_at = std::chrono::steady_clock::now();
auto* header = reinterpret_cast<
crossdesk::CrossDeskSecureDesktopSharedFrameHeader*>(frame_view);
auto* header =
reinterpret_cast<crossdesk::CrossDeskSecureDesktopSharedFrameHeader*>(
frame_view);
uint8_t* payload = frame_view + sizeof(*header);
header->writing = 1;
MemoryBarrier();
@@ -1367,41 +1509,25 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
MemoryBarrier();
header->writing = 0;
SetEvent(capture_state->frame_ready_event);
++frame_count;
capture_ms += std::chrono::duration<double, std::milli>(captured_at -
frame_started)
.count();
convert_ms += std::chrono::duration<double, std::milli>(converted_at -
captured_at)
.count();
publish_ms += std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - converted_at)
.count();
}
} else {
const DWORD error = capture->error_code;
capture.reset();
const ULONGLONG now = GetTickCount64();
if (last_error_tick == 0 || now - last_error_tick >= 5000) {
LOG_WARN(
"Secure shared capture BitBlt failed; recreating capture, "
"error={}",
error);
last_error_tick = now;
}
LOG_WARN("Secure shared capture BitBlt failed, error={}", GetLastError());
}
const auto elapsed_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - frame_started)
.count();
const int retry_interval_ms = capture ? interval_ms : 100;
if (elapsed_ms < retry_interval_ms) {
if (elapsed_ms < interval_ms) {
std::this_thread::sleep_for(
std::chrono::milliseconds(retry_interval_ms - elapsed_ms));
std::chrono::milliseconds(interval_ms - elapsed_ms));
}
}
report_stats(true);
SelectObject(mem_dc, old_bitmap);
DeleteObject(dib);
DeleteDC(mem_dc);
ReleaseDC(nullptr, screen_dc);
}
std::vector<uint8_t> StopSecureDesktopSharedCapture(
@@ -1577,7 +1703,7 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
json["extended"] = extended;
json["stage"] = interactive_stage;
json["interactive_desktop"] = interactive_desktop;
json["desktop"] = inject_result.desktop.current_desktop;
json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW());
return BuildTextResponseBytes(json.dump());
}
@@ -1605,7 +1731,7 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
json["flag"] = mouse_request.flag;
json["stage"] = mouse_request.interactive_stage;
json["interactive_desktop"] = mouse_request.interactive_desktop;
json["desktop"] = inject_result.desktop.current_desktop;
json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW());
return BuildTextResponseBytes(json.dump());
}
@@ -1,165 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include "captured_nv12_frame.h"
#include <algorithm>
#include <cstring>
#include <limits>
#include <new>
#include <utility>
namespace crossdesk {
namespace {
bool GetNv12Size(uint32_t width, uint32_t height, size_t* size) {
if (!size || width == 0 || height == 0 || (width & 1U) != 0 ||
(height & 1U) != 0 ||
static_cast<size_t>(width) >
std::numeric_limits<size_t>::max() / height) {
return false;
}
const size_t pixels = static_cast<size_t>(width) * height;
if (pixels > std::numeric_limits<size_t>::max() / 3U * 2U) {
return false;
}
*size = pixels * 3U / 2U;
return true;
}
} // namespace
std::shared_ptr<CapturedNv12FramePool> CapturedNv12FramePool::Create(
size_t max_cached_buffers) {
return std::shared_ptr<CapturedNv12FramePool>(
new CapturedNv12FramePool(max_cached_buffers));
}
CapturedNv12FramePool::CapturedNv12FramePool(size_t max_cached_buffers)
: max_cached_buffers_(max_cached_buffers) {}
CapturedNv12Frame* CapturedNv12FramePool::CopyFrom(const uint8_t* data,
size_t size,
uint32_t width,
uint32_t height) {
size_t required_size = 0;
if (!data || !GetNv12Size(width, height, &required_size) ||
size < required_size) {
return nullptr;
}
std::vector<uint8_t> storage;
{
std::lock_guard<std::mutex> lock(mutex_);
auto best = free_buffers_.end();
for (auto it = free_buffers_.begin(); it != free_buffers_.end(); ++it) {
if (it->capacity() < required_size) {
continue;
}
if (best == free_buffers_.end() || it->capacity() < best->capacity()) {
best = it;
}
}
if (best == free_buffers_.end() && !free_buffers_.empty()) {
best = std::max_element(
free_buffers_.begin(), free_buffers_.end(),
[](const auto& lhs, const auto& rhs) {
return lhs.capacity() < rhs.capacity();
});
}
if (best != free_buffers_.end()) {
storage = std::move(*best);
free_buffers_.erase(best);
}
}
try {
storage.resize(required_size);
std::memcpy(storage.data(), data, required_size);
return new CapturedNv12Frame(shared_from_this(), std::move(storage), width,
height);
} catch (...) {
return nullptr;
}
}
void CapturedNv12FramePool::Recycle(std::vector<uint8_t> storage) {
if (max_cached_buffers_ == 0 || storage.empty()) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (free_buffers_.size() < max_cached_buffers_) {
free_buffers_.push_back(std::move(storage));
return;
}
auto smallest = std::min_element(
free_buffers_.begin(), free_buffers_.end(),
[](const auto& lhs, const auto& rhs) {
return lhs.capacity() < rhs.capacity();
});
if (smallest != free_buffers_.end() &&
smallest->capacity() < storage.capacity()) {
*smallest = std::move(storage);
}
}
CapturedNv12Frame::CapturedNv12Frame(
std::shared_ptr<CapturedNv12FramePool> pool,
std::vector<uint8_t> storage, uint32_t width, uint32_t height)
: pool_(std::move(pool)),
storage_(std::move(storage)),
width_(width),
height_(height) {
descriptor_.struct_size = sizeof(descriptor_);
descriptor_.type = MiniRtcNativeVideoFrameCpuNv12;
descriptor_.width = width_;
descriptor_.height = height_;
descriptor_.payload.cpu_nv12 = {
storage_.data(), storage_.data() + static_cast<size_t>(width_) * height_,
width_, width_};
descriptor_.owner = this;
descriptor_.retain = &RetainOwner;
descriptor_.release = &ReleaseOwner;
descriptor_.copy_to_nv12 = &CopyToNv12;
}
CapturedNv12Frame::~CapturedNv12Frame() {
if (pool_) {
pool_->Recycle(std::move(storage_));
}
}
void CapturedNv12Frame::AddRef() {
references_.fetch_add(1, std::memory_order_relaxed);
}
void CapturedNv12Frame::Release() {
if (references_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete this;
}
}
void CapturedNv12Frame::RetainOwner(void* owner) {
static_cast<CapturedNv12Frame*>(owner)->AddRef();
}
void CapturedNv12Frame::ReleaseOwner(void* owner) {
static_cast<CapturedNv12Frame*>(owner)->Release();
}
int CapturedNv12Frame::CopyToNv12(void* owner, uint8_t* destination,
size_t destination_size) {
auto* frame = static_cast<CapturedNv12Frame*>(owner);
if (!frame || !destination || destination_size < frame->Size()) {
return -1;
}
std::memcpy(destination, frame->storage_.data(), frame->Size());
return 0;
}
} // namespace crossdesk
@@ -1,78 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CAPTURED_NV12_FRAME_H_
#define _CAPTURED_NV12_FRAME_H_
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mutex>
#include <vector>
#include "minirtc.h"
namespace crossdesk {
class CapturedNv12Frame;
// Owns the pixels behind captured native frames. A frame may outlive the
// capture callback while MiniRTC waits for its asynchronous encoder, so capture
// buffers cannot be borrowed directly. Released allocations are cached to keep
// that ownership transfer inexpensive after the pipeline is warm.
class CapturedNv12FramePool final
: public std::enable_shared_from_this<CapturedNv12FramePool> {
public:
static std::shared_ptr<CapturedNv12FramePool> Create(
size_t max_cached_buffers = 4);
CapturedNv12Frame* CopyFrom(const uint8_t* data, size_t size,
uint32_t width, uint32_t height);
private:
explicit CapturedNv12FramePool(size_t max_cached_buffers);
void Recycle(std::vector<uint8_t> storage);
friend class CapturedNv12Frame;
const size_t max_cached_buffers_;
std::mutex mutex_;
std::vector<std::vector<uint8_t>> free_buffers_;
};
class CapturedNv12Frame final {
public:
const MiniRtcNativeVideoFrame* Descriptor() const { return &descriptor_; }
size_t Size() const { return storage_.size(); }
void AddRef();
void Release();
private:
friend class CapturedNv12FramePool;
CapturedNv12Frame(std::shared_ptr<CapturedNv12FramePool> pool,
std::vector<uint8_t> storage, uint32_t width,
uint32_t height);
~CapturedNv12Frame();
static void RetainOwner(void* owner);
static void ReleaseOwner(void* owner);
static int CopyToNv12(void* owner, uint8_t* destination,
size_t destination_size);
std::atomic<uint32_t> references_{1};
std::shared_ptr<CapturedNv12FramePool> pool_;
std::vector<uint8_t> storage_;
uint32_t width_ = 0;
uint32_t height_ = 0;
MiniRtcNativeVideoFrame descriptor_{};
};
} // namespace crossdesk
#endif
@@ -10,18 +10,14 @@
#include <functional>
#include "display_info.h"
#include "minirtc.h"
namespace crossdesk {
class ScreenCapturer {
public:
// |stream_id| is a logical MiniRTC stream ID (DisplayN), not a platform
// display name or physical handle. |native_frame| is borrowed for the
// duration of the callback; retain its owner before using it asynchronously.
typedef std::function<void(unsigned char* data, int size, int width,
int height, const char* stream_id,
const MiniRtcNativeVideoFrame* native_frame)>
// The final callback argument is a logical MiniRTC stream ID (DisplayN),
// not a platform display name or physical handle.
typedef std::function<void(unsigned char*, int, int, int, const char*)>
cb_desktop_data;
public:
@@ -0,0 +1,200 @@
#include <cstdint>
#include <iostream>
#include <memory>
#include "video_receive_scheduler.h"
namespace {
std::unique_ptr<minirtc::ReceivedFrame>
MakeFrame(uint32_t rtp_timestamp, int64_t received_us, bool key_frame = false) {
const uint8_t payload = key_frame ? 0x65 : 0x41;
auto frame =
std::make_unique<minirtc::ReceivedFrame>(&payload, sizeof(payload));
frame->SetRtpTimestamp(rtp_timestamp);
frame->SetReceivedTimestamp(received_us);
frame->SetCapturedTimestamp(received_us - 5000);
frame->SetFrameType(key_frame ? minirtc::kVideoFrameKey
: minirtc::kVideoFrameDelta);
return frame;
}
bool Expect(bool condition, const char *message) {
if (!condition) {
std::cerr << message << '\n';
}
return condition;
}
bool TestOneFramePerPlaybackCycle() {
minirtc::VideoReceiveScheduler scheduler(60);
scheduler.Enqueue(MakeFrame(90000, 1000000), 1000000);
scheduler.Enqueue(MakeFrame(91500, 1016667), 1016667);
auto first = scheduler.Poll(1016667, false);
bool ok = Expect(first.frame != nullptr, "first frame was not released");
ok &= Expect(first.frame && first.frame->RtpTimestamp() == 90000,
"scheduler did not preserve RTP order");
auto same_cycle = scheduler.Poll(1016667, false);
ok &= Expect(!same_cycle.frame,
"scheduler released more than one frame in a cycle");
ok &= Expect(same_cycle.next_delay_us > 0,
"scheduler did not schedule the next playback cycle");
auto second = scheduler.Poll(1033334, false);
ok &= Expect(second.frame != nullptr,
"second frame was not released on the next cycle");
ok &= Expect(second.frame && second.frame->RtpTimestamp() == 91500,
"second RTP timestamp was incorrect");
return ok;
}
bool TestSevereLagJumpsToNewestKeyFrame() {
minirtc::VideoReceiveScheduler scheduler(60);
for (uint32_t i = 0; i < 10; ++i) {
scheduler.Enqueue(MakeFrame(90000 + i * 1500, 1000000 + i * 16667, i == 8),
1000000 + i * 16667);
}
auto result = scheduler.Poll(1200000, false);
const auto metrics = scheduler.GetMetrics(1200000);
bool ok = Expect(result.frame && result.frame->IsKeyFrame(),
"catch-up did not jump to the newest key frame");
ok &= Expect(result.dropped_frames == 8,
"catch-up dropped an unexpected number of frames");
ok &= Expect(metrics.catch_up_count == 1,
"catch-up metric was not incremented");
return ok;
}
bool TestSevereLagRequestsKeyFrameWhenNoSyncPointExists() {
minirtc::VideoReceiveScheduler scheduler(60);
for (uint32_t i = 0; i < 10; ++i) {
scheduler.Enqueue(MakeFrame(180000 + i * 1500, 2000000 + i * 16667),
2000000 + i * 16667);
}
auto result = scheduler.Poll(2200000, false);
const auto metrics = scheduler.GetMetrics(2200000);
bool ok = Expect(!result.frame,
"undecodable delta frame was released during catch-up");
ok &=
Expect(result.request_key_frame, "catch-up did not request a key frame");
ok &= Expect(metrics.buffered_frames == 0 && metrics.awaiting_key_frame,
"scheduler did not enter key-frame wait state");
return ok;
}
bool TestAdaptiveBufferStaysWithinOneToThreeFrames() {
minirtc::VideoReceiveScheduler scheduler(60);
int64_t arrival_us = 3000000;
for (uint32_t i = 0; i < 40; ++i) {
arrival_us += (i % 2 == 0) ? 5000 : 30000;
scheduler.Enqueue(MakeFrame(270000 + i * 1500, arrival_us), arrival_us);
}
const auto metrics = scheduler.GetMetrics(arrival_us);
bool ok = Expect(metrics.target_buffer_frames >= 1 &&
metrics.target_buffer_frames <= 3,
"adaptive target left the 1-3 frame range");
ok &= Expect(metrics.target_buffer_frames > 1,
"arrival jitter did not increase the target buffer");
return ok;
}
bool TestLegacyMicrosecondTimestampFallsBackToArrivalCadence() {
minirtc::VideoReceiveScheduler scheduler(60);
int64_t arrival_us = 4000000;
for (uint32_t i = 0; i < 20; ++i) {
scheduler.Enqueue(MakeFrame(500000 + i * 16667, arrival_us), arrival_us);
scheduler.Poll(arrival_us, false);
arrival_us += 16667;
}
scheduler.Poll(arrival_us, false);
const auto metrics = scheduler.GetMetrics(arrival_us);
bool ok = Expect(metrics.estimated_frame_interval_us >= 15000 &&
metrics.estimated_frame_interval_us <= 20000,
"legacy RTP units slowed the playback interval");
ok &= Expect(metrics.timestamp_fallbacks > 0,
"legacy RTP units did not activate timestamp fallback");
ok &= Expect(metrics.released_frames == 20 && metrics.dropped_frames == 0,
"legacy RTP units caused scheduler frame loss");
return ok;
}
bool TestSixtyFpsCadenceDoesNotTriggerCatchUp() {
minirtc::VideoReceiveScheduler scheduler(60);
constexpr int64_t kStartUs = 6000000;
constexpr int64_t kFrameIntervalUs = 16667;
constexpr uint32_t kStartRtpTimestamp = 700000;
constexpr uint32_t kRtpFrameInterval = 1500;
for (uint32_t i = 0; i < 60; ++i) {
const int64_t now_us = kStartUs + static_cast<int64_t>(i) *
kFrameIntervalUs;
scheduler.Enqueue(
MakeFrame(kStartRtpTimestamp + i * kRtpFrameInterval, now_us),
now_us);
scheduler.Poll(now_us, false);
}
scheduler.Poll(kStartUs + 60 * kFrameIntervalUs, false);
const auto metrics =
scheduler.GetMetrics(kStartUs + 60 * kFrameIntervalUs);
bool ok = Expect(metrics.released_frames == 60,
"60 fps input was not released at 60 fps cadence");
ok &= Expect(metrics.dropped_frames == 0 && metrics.catch_up_count == 0,
"steady 60 fps input incorrectly triggered catch-up");
ok &= Expect(metrics.estimated_frame_interval_us >= 15000 &&
metrics.estimated_frame_interval_us <= 18000,
"steady 60 fps interval estimate drifted");
return ok;
}
bool TestKeyFrameRequestCooldownKeepsDecodableProgress() {
minirtc::VideoReceiveScheduler scheduler(60);
for (uint32_t i = 0; i < 10; ++i) {
scheduler.Enqueue(MakeFrame(600000 + i * 1500,
5000000 + static_cast<int64_t>(i) * 16667),
5000000 + static_cast<int64_t>(i) * 16667);
}
auto first_catch_up = scheduler.Poll(5200000, false);
bool ok = Expect(first_catch_up.request_key_frame,
"initial severe lag did not request a key frame");
scheduler.Enqueue(MakeFrame(615000, 5220000, true), 5220000);
auto key_frame = scheduler.Poll(5240000, false);
ok &= Expect(key_frame.frame && key_frame.frame->IsKeyFrame(),
"requested key frame was not released");
for (uint32_t i = 0; i < 10; ++i) {
scheduler.Enqueue(MakeFrame(616500 + i * 1500,
5250000 + static_cast<int64_t>(i) * 16667),
5250000 + static_cast<int64_t>(i) * 16667);
}
auto cooldown_catch_up = scheduler.Poll(5450000, false);
const auto metrics = scheduler.GetMetrics(5450000);
ok &= Expect(!cooldown_catch_up.request_key_frame,
"key frame request cooldown was ignored");
ok &= Expect(cooldown_catch_up.frame != nullptr,
"cooldown prevented forward playback progress");
ok &= Expect(metrics.key_frame_requests == 1,
"key frame request metric ignored cooldown");
return ok;
}
} // namespace
int main() {
bool ok = true;
ok &= TestOneFramePerPlaybackCycle();
ok &= TestSevereLagJumpsToNewestKeyFrame();
ok &= TestSevereLagRequestsKeyFrameWhenNoSyncPointExists();
ok &= TestAdaptiveBufferStaysWithinOneToThreeFrames();
ok &= TestLegacyMicrosecondTimestampFallsBackToArrivalCadence();
ok &= TestSixtyFpsCadenceDoesNotTriggerCatchUp();
ok &= TestKeyFrameRequestCooldownKeepsDecodableProgress();
return ok ? 0 : 1;
}
@@ -0,0 +1,84 @@
#include <cstdint>
#include <iostream>
#include <memory>
#include <vector>
#include "rtp_defines.h"
#include "rtp_packetizer_av1.h"
#include "rtp_packetizer_h264.h"
namespace {
bool Expect(bool condition, const char *message) {
if (!condition) {
std::cerr << message << '\n';
}
return condition;
}
bool AllPacketsUseTimestamp(
const std::vector<std::unique_ptr<minirtc::RtpPacket>> &packets,
uint32_t expected_timestamp) {
if (packets.empty()) {
return false;
}
for (const auto &packet : packets) {
if (!packet || packet->Timestamp() != expected_timestamp) {
return false;
}
}
return true;
}
bool TestTimestampConversion() {
bool ok = Expect(minirtc::rtp::VideoTimestampFromMicroseconds(1000000) ==
90000,
"one second did not convert to 90 kHz");
ok &= Expect(minirtc::rtp::VideoTimestampFromMicroseconds(16667) == 1500,
"60 fps interval did not convert to 1500 RTP ticks");
ok &= Expect(minirtc::rtp::VideoTimestampToMicroseconds(90000) == 1000000,
"90 kHz timestamp did not convert to one second");
return ok;
}
bool TestH264PacketizerPreservesTimestamp() {
constexpr uint32_t kTimestamp = 0x89abcdef;
minirtc::RtpPacketizerH264 packetizer(1234);
std::vector<uint8_t> small_nalu = {0x65, 0x01, 0x02};
auto single_packet =
packetizer.Build(small_nalu.data(), small_nalu.size(), kTimestamp, true);
bool ok = Expect(AllPacketsUseTimestamp(single_packet, kTimestamp),
"H.264 single-NAL packetizer changed RTP timestamp");
std::vector<uint8_t> large_nalu(MAX_NALU_LEN + 100, 0x55);
large_nalu.front() = 0x65;
auto fragmented =
packetizer.Build(large_nalu.data(), large_nalu.size(), kTimestamp, true);
ok &= Expect(AllPacketsUseTimestamp(fragmented, kTimestamp),
"H.264 FU-A packetizer changed RTP timestamp");
auto padding = packetizer.BuildPadding(2000, kTimestamp, true);
ok &= Expect(AllPacketsUseTimestamp(padding, kTimestamp),
"H.264 padding packetizer changed RTP timestamp");
return ok;
}
bool TestAv1PacketizerPreservesTimestamp() {
constexpr uint32_t kTimestamp = 0x12345678;
minirtc::RtpPacketizerAv1 packetizer(5678);
// One frame OBU with a one-byte payload.
std::vector<uint8_t> obu = {0x32, 0x01, 0x00};
auto packets = packetizer.Build(obu.data(), obu.size(), kTimestamp, true);
return Expect(AllPacketsUseTimestamp(packets, kTimestamp),
"AV1 packetizer changed RTP timestamp");
}
} // namespace
int main() {
bool ok = true;
ok &= TestTimestampConversion();
ok &= TestH264PacketizerPreservesTimestamp();
ok &= TestAv1PacketizerPreservesTimestamp();
return ok ? 0 : 1;
}
+13 -12
View File
@@ -34,7 +34,6 @@ function setup_targets()
set_kind("object")
add_deps("rd_log", "crossdesk_wire")
add_packages("libyuv")
add_packages("concurrentqueue", {public = true})
add_files("apps/desktop/src/common/*.cpp")
remove_files("apps/desktop/src/common/rounded_corner_button.cpp")
if is_os("windows") then
@@ -107,6 +106,18 @@ function setup_targets()
set_default(false)
add_files("apps/desktop/tests/video_callback_lifetime_test.cpp")
target("video_receive_scheduler_test")
set_kind("binary")
set_default(false)
add_deps("minirtc")
add_files("apps/desktop/tests/video_receive_scheduler_test.cpp")
target("video_rtp_timestamp_test")
set_kind("binary")
set_default(false)
add_deps("minirtc")
add_files("apps/desktop/tests/video_rtp_timestamp_test.cpp")
target("windows_manifest_resource_test")
set_kind("binary")
set_default(false)
@@ -160,10 +171,8 @@ function setup_targets()
set_kind("object")
add_deps("rd_log", "common", "crossdesk_wire")
add_includedirs("apps/desktop/src/screen_capturer", {public = true})
add_includedirs("deps/submodules/minirtc/src/api", {public = true})
if is_os("windows") then
add_packages("libyuv")
add_files("apps/desktop/src/screen_capturer/captured_nv12_frame.cpp")
add_files("apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp",
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp",
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp",
@@ -330,11 +339,7 @@ function setup_targets()
add_cxxflags("/bigobj")
add_links("opengl32")
add_files("apps/desktop/src/platform/windows/gui/tray/win_tray.cpp",
"apps/desktop/src/platform/windows/gui/slint_backend.cpp",
"apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp",
"apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp",
"apps/desktop/src/platform/common/gui/cuda_driver.cpp",
"apps/desktop/src/platform/common/gui/cuda_opengl_interop.cpp",
"apps/desktop/src/platform/common/gui/video_renderer_factory_opengl.cpp",
"apps/desktop/src/platform/windows/gui/runtime/windows_service_runtime.cpp",
"apps/desktop/src/platform/windows/gui/application/portable_service_integration.cpp")
@@ -352,10 +357,7 @@ function setup_targets()
elseif is_os("linux") then
add_links("GL")
add_files("apps/desktop/src/platform/linux/gui/tray/linux_tray.cpp",
"apps/desktop/src/platform/linux/gui/cuda_graphics.cpp",
"apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp",
"apps/desktop/src/platform/common/gui/cuda_driver.cpp",
"apps/desktop/src/platform/common/gui/cuda_opengl_interop.cpp",
"apps/desktop/src/platform/common/gui/video_renderer_factory_opengl.cpp")
add_includedirs("apps/desktop/src/platform/common/gui",
"apps/desktop/src/platform/linux/gui/tray",
@@ -377,8 +379,7 @@ function setup_targets()
"apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_entry.cpp")
add_files("apps/desktop/resources/windows/wgc_plugin.rc")
add_includedirs("apps/desktop/src/common", "apps/desktop/src/screen_capturer",
"apps/desktop/src/platform/windows/screen_capturer",
"deps/submodules/minirtc/src/api")
"apps/desktop/src/platform/windows/screen_capturer")
target("crossdesk_service")
set_kind("binary")
@@ -202,7 +202,7 @@ void DispatchMain(dispatch_block_t block) {
generation:(uint64_t)generation;
- (BOOL)isControllerGenerationActive:(uint64_t)generation;
- (BOOL)isControllerGenerationCurrent:(uint64_t)generation;
- (void)handleVideoFrame:(const MiniRtcVideoFrame *)frame
- (void)handleVideoFrame:(const XVideoFrame *)frame
sourceID:(const char *)sourceID
sourceIDSize:(size_t)sourceIDSize;
- (void)enqueueVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer
@@ -214,7 +214,7 @@ void DispatchMain(dispatch_block_t block) {
size:(size_t)size
sourceID:(const char *)sourceID
sourceIDSize:(size_t)sourceIDSize;
- (void)handleStats:(const MiniRtcNetTrafficStats *)stats mode:(TraversalMode)mode;
- (void)handleStats:(const XNetTrafficStats *)stats mode:(TraversalMode)mode;
- (void)sendMessage:(const std::string &)message
reliable:(BOOL)reliable
stream:(const char *)stream;
@@ -227,7 +227,7 @@ void DispatchMain(dispatch_block_t block) {
namespace {
void OnVideoFrame(const MiniRtcVideoFrame *frame, const char *, size_t,
void OnVideoFrame(const XVideoFrame *frame, const char *, size_t,
const char *source_id, size_t source_id_size,
void *user_data) {
auto *context = static_cast<CallbackContext *>(user_data);
@@ -298,7 +298,7 @@ void OnConnectionState(ConnectionStatus status, const char *remote_id,
}
void OnNetworkStats(const char *peer_id, size_t peer_id_size,
TraversalMode mode, const MiniRtcNetTrafficStats *stats,
TraversalMode mode, const XNetTrafficStats *stats,
const char *, size_t, void *user_data) {
auto *context = static_cast<CallbackContext *>(user_data);
CrossDeskRTCBridge *owner = context ? context->owner : nil;
@@ -1114,7 +1114,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
return active == 0 || active == generation;
}
- (void)handleVideoFrame:(const MiniRtcVideoFrame *)frame
- (void)handleVideoFrame:(const XVideoFrame *)frame
sourceID:(const char *)sourceID
sourceIDSize:(size_t)sourceIDSize {
if (!frame || frame->width == 0 || frame->height == 0) return;
@@ -1124,17 +1124,12 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
static_cast<size_t>(_selectedDisplay.load()));
if (source != expected) return;
}
const MiniRtcNativeVideoFrame *native_frame = frame->native_frame;
const bool has_native_pixel_buffer =
native_frame &&
native_frame->struct_size >=
static_cast<uint32_t>(sizeof(MiniRtcNativeVideoFrame)) &&
native_frame->type == MiniRtcNativeVideoFrameCVPixelBuffer &&
native_frame->payload.cv_pixel_buffer;
frame->native_handle &&
frame->native_handle_type == XVideoFrameNativeHandleCVPixelBuffer;
if (has_native_pixel_buffer) {
CVPixelBufferRef pixel_buffer =
static_cast<CVPixelBufferRef>(
native_frame->payload.cv_pixel_buffer);
static_cast<CVPixelBufferRef>(frame->native_handle);
[self enqueueVideoPixelBuffer:pixel_buffer
width:frame->width
height:frame->height];
@@ -1584,7 +1579,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
}
}
- (void)handleStats:(const MiniRtcNetTrafficStats *)stats mode:(TraversalMode)mode {
- (void)handleStats:(const XNetTrafficStats *)stats mode:(TraversalMode)mode {
if (!stats) return;
const NSUInteger bitrate = stats->total_inbound_stats.bitrate;
const float loss = stats->video_inbound_stats.loss_rate;
-1
View File
@@ -121,7 +121,6 @@ LINK_DIRS=("${(@f)$(print -r -- "${CLEAN_INFO}" | sed -nE 's|.*-> (/.*)/lib -> p
REQUIRED_LINKS=(
nice glib-2.0 gobject-2.0 gmodule-2.0 gio-2.0 gthread-2.0 intl
gupnp-igd-1.6 gupnp-1.6 gssdp-1.6 soup-3.0 nghttp2 sqlite3 psl xml2
ffi pcre2-8 pcre2-posix z ssl crypto srtp2 openfec opus yuv kcp
datachannel usrsctp openh264 dav1d aom SvtAv1Enc
)