mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-18 01:46:47 +08:00
[feat] negotiate per-device preview and audio subscriptions
This commit is contained in:
@@ -49,7 +49,14 @@ SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner)
|
||||
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());
|
||||
std::shared_lock lock(owner_.connection_status_mutex_);
|
||||
for (const auto& [id, status] : owner_.connection_status_) {
|
||||
if (status == ConnectionStatus::Connected &&
|
||||
owner_.preview_subscriptions_.ShouldSendAudio(id))
|
||||
SendAudioFrameToPeer(owner_.peer_, &frame,
|
||||
owner_.audio_label_.c_str(), id.data(),
|
||||
id.size());
|
||||
}
|
||||
}) {}
|
||||
|
||||
SessionDeviceManager::~SessionDeviceManager() { speaker_capture_.Shutdown(); }
|
||||
@@ -245,6 +252,8 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
const auto send_started = std::chrono::steady_clock::now();
|
||||
uint64_t send_errors = 0;
|
||||
for (const std::string &remote_id : connected_remote_ids) {
|
||||
if (!owner_.preview_subscriptions_.ShouldSend(remote_id, now_time,
|
||||
stream_id)) continue;
|
||||
if (SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
|
||||
remote_id.data(), remote_id.size()) != 0)
|
||||
++send_errors;
|
||||
|
||||
@@ -130,6 +130,7 @@ void GuiRuntime::HandlePresenceProbeTimeout() {
|
||||
|
||||
void GuiRuntime::HandleServerControllerDisconnected(
|
||||
const std::string& remote_id, const char* reason) {
|
||||
preview_subscriptions_.Remove(remote_id);
|
||||
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
|
||||
#ifdef _WIN32
|
||||
devices_.ReleaseRemoteMouseButtons();
|
||||
|
||||
@@ -151,14 +151,67 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string remote_id(user_id, user_id_size);
|
||||
std::string json_str(data, size);
|
||||
const auto preview_json =
|
||||
json_str.find("\"deskhub_preview") != std::string::npos
|
||||
? nlohmann::json::parse(json_str, nullptr, false)
|
||||
: nlohmann::json{};
|
||||
if (preview_json.is_object() &&
|
||||
preview_json.value("type", nlohmann::json{}) == "deskhub_preview") {
|
||||
if (source_id != runtime->control_data_label_ ||
|
||||
!runtime->IsAuthorizedController(remote_id))
|
||||
return;
|
||||
const auto subscription = PreviewSubscription::Parse(preview_json);
|
||||
if (!subscription) return;
|
||||
bool accepted = true;
|
||||
// Apply encoder quality to this controller only; other controllers retain
|
||||
// their own demand. Frame gating happens before encoding and transmission.
|
||||
if (subscription->fps > 0 && runtime->preview_subscriptions_.IsNewer(
|
||||
remote_id, subscription->request_id))
|
||||
accepted = UpdateVideoSettings(
|
||||
runtime->peer_, remote_id.data(), remote_id.size(),
|
||||
static_cast<VideoQuality>(subscription->quality),
|
||||
subscription->frame_rate,
|
||||
static_cast<VideoDegradationPreference>(
|
||||
subscription->EncoderPreference())) == 0;
|
||||
if (accepted && runtime->preview_subscriptions_.Set(remote_id, *subscription) &&
|
||||
subscription->fps > 0)
|
||||
RequestAllVideoKeyFrames(runtime->peer_);
|
||||
const auto reply = nlohmann::json{
|
||||
{"type", "deskhub_preview_status"},
|
||||
{"version", 1},
|
||||
{"request_id", subscription->request_id},
|
||||
{"accepted",
|
||||
accepted}}.dump();
|
||||
SendReliableDataFrameToPeer(runtime->peer_, reply.data(), reply.size(),
|
||||
runtime->control_data_label_.c_str(),
|
||||
remote_id.data(), remote_id.size());
|
||||
return;
|
||||
}
|
||||
if (preview_json.is_object() &&
|
||||
preview_json.value("type", nlohmann::json{}) ==
|
||||
"deskhub_preview_status") {
|
||||
if (auto props = runtime->FindRemoteSession(remote_id);
|
||||
props && source_id == props->control_data_label_) {
|
||||
try {
|
||||
if (preview_json.at("version") == 1 &&
|
||||
preview_json.at("request_id").is_number_unsigned() &&
|
||||
preview_json.at("accepted").is_boolean()) {
|
||||
props->preview_accepted_ = preview_json.at("accepted").get<bool>();
|
||||
props->preview_ack_ = preview_json.at("request_id").get<uint32_t>();
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
RemoteAction remote_action{};
|
||||
if (!remote_action.from_json(json_str)) {
|
||||
LOG_ERROR("Failed to parse RemoteAction JSON payload");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
if (remote_action.type == ControlType::video_settings_status) {
|
||||
if (auto props = runtime->FindRemoteSession(remote_id)) {
|
||||
if (source_id != props->control_data_label_) return;
|
||||
@@ -342,6 +395,8 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
if (props && source_id == props->control_data_label_) {
|
||||
std::lock_guard lock(props->video_settings_mutex_);
|
||||
const bool first_advertisement = !props->video_settings_supported_;
|
||||
props->preview_subscription_supported_ =
|
||||
remote_action.i.supports_deskhub_preview;
|
||||
props->video_settings_supported_ =
|
||||
remote_action.i.supports_video_settings;
|
||||
if (first_advertisement && props->video_settings_supported_) {
|
||||
|
||||
@@ -288,6 +288,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
/*supports_privacy_screen=*/!runtime->unattended_host_,
|
||||
CROSSDESK_VERSION, kClientPlatform);
|
||||
remote_action.i.supports_video_settings = true;
|
||||
remote_action.i.supports_deskhub_preview = true;
|
||||
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
remote_action.i.display_list[i]);
|
||||
@@ -327,6 +328,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
}
|
||||
props->net_traffic_stats_.Reset();
|
||||
props->enable_mouse_control_ = false;
|
||||
props->preview_subscription_supported_ = false;
|
||||
runtime->ResetRemoteServiceStatus(*props);
|
||||
{
|
||||
std::lock_guard lock(props->video_settings_mutex_);
|
||||
@@ -414,6 +416,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
runtime->preview_subscriptions_.Remove(remote_id);
|
||||
#if _WIN32
|
||||
runtime->last_windows_service_status_tick_ = 0;
|
||||
#endif
|
||||
@@ -423,6 +426,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
/*supports_privacy_screen=*/!runtime->unattended_host_,
|
||||
CROSSDESK_VERSION, kClientPlatform);
|
||||
remote_action.i.supports_video_settings = true;
|
||||
remote_action.i.supports_deskhub_preview = true;
|
||||
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
remote_action.i.display_list[i]);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-12
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PREVIEW_SUBSCRIPTIONS_H_
|
||||
#define _PREVIEW_SUBSCRIPTIONS_H_
|
||||
|
||||
#include <preview_subscription.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace crossdesk {
|
||||
// Accessed by the network callback and screen-capture callback. Legacy
|
||||
// controllers have no entry and continue receiving their normal stream.
|
||||
class PreviewSubscriptions {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
bool IsNewer(const std::string &id, uint32_t request_id) {
|
||||
std::lock_guard lock(mutex_);
|
||||
const auto found = states_.find(id);
|
||||
return found == states_.end() ||
|
||||
request_id > found->second.request.request_id;
|
||||
}
|
||||
bool Set(const std::string &id, const PreviewSubscription &request) {
|
||||
std::lock_guard lock(mutex_);
|
||||
auto &state = states_[id];
|
||||
if (request.request_id <= state.request.request_id)
|
||||
return false;
|
||||
state.request = request;
|
||||
state.next.clear();
|
||||
return true;
|
||||
}
|
||||
bool ShouldSend(const std::string &id, Clock::time_point now = Clock::now(),
|
||||
const std::string &stream = {}) {
|
||||
std::lock_guard lock(mutex_);
|
||||
const auto found = states_.find(id);
|
||||
if (found == states_.end())
|
||||
return true;
|
||||
auto &state = found->second;
|
||||
if (!state.request.fps)
|
||||
return false;
|
||||
// Interactive streams use the encoder's own 30/60 FPS cadence. A second
|
||||
// time gate here can drop valid frames because capture timing varies.
|
||||
if (state.request.fps == 60)
|
||||
return true;
|
||||
auto &next = state.next[stream];
|
||||
if (now < next)
|
||||
return false;
|
||||
next = now + std::chrono::milliseconds(1000 / state.request.fps);
|
||||
return true;
|
||||
}
|
||||
bool ShouldSendAudio(const std::string &id) {
|
||||
std::lock_guard lock(mutex_);
|
||||
const auto found = states_.find(id);
|
||||
return found == states_.end() || found->second.request.audio;
|
||||
}
|
||||
void Remove(const std::string &id) {
|
||||
std::lock_guard lock(mutex_);
|
||||
states_.erase(id);
|
||||
}
|
||||
|
||||
private:
|
||||
struct State {
|
||||
PreviewSubscription request;
|
||||
// One display must not consume another display's preview frame budget.
|
||||
std::unordered_map<std::string, Clock::time_point> next;
|
||||
};
|
||||
std::mutex mutex_;
|
||||
std::unordered_map<std::string, State> states_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include <remote_action.h>
|
||||
#include <preview_subscription.h>
|
||||
#include <stream_names.h>
|
||||
#include "display_info.h"
|
||||
#include "cursor_presentation.h"
|
||||
@@ -70,6 +71,12 @@ struct FileTransferState {
|
||||
// Runtime state for one connected or connecting remote endpoint. It groups the
|
||||
// connection, media, input, window and transfer data that share one lifetime.
|
||||
struct RemoteSession {
|
||||
std::atomic<bool> preview_subscription_supported_{false};
|
||||
std::atomic<uint32_t> preview_ack_{0};
|
||||
std::atomic<bool> preview_accepted_{false};
|
||||
uint32_t preview_request_ = 0;
|
||||
PreviewSubscription preview_requested_;
|
||||
std::chrono::steady_clock::time_point preview_request_at_{};
|
||||
Params params_;
|
||||
PeerPtr* peer_ = nullptr;
|
||||
std::shared_ptr<PeerEventHandler> peer_events_;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "common/task_queue_lock_free.h"
|
||||
#include "path_manager.h"
|
||||
#include "runtime/device_presence_cache.h"
|
||||
#include "runtime/preview_subscriptions.h"
|
||||
#include "runtime/remote_session.h"
|
||||
#include "runtime/session_lifecycle.h"
|
||||
#include "thumbnail.h"
|
||||
@@ -36,6 +37,7 @@
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
struct InfrastructureState {
|
||||
PreviewSubscriptions preview_subscriptions_;
|
||||
std::unique_ptr<ConfigCenter> config_center_;
|
||||
ConfigCenter::LANGUAGE localization_language_ =
|
||||
ConfigCenter::LANGUAGE::CHINESE;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-12
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PREVIEW_SUBSCRIPTION_H_
|
||||
#define _PREVIEW_SUBSCRIPTION_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
|
||||
namespace crossdesk {
|
||||
// Versioned independently from the interactive 30/60 FPS video settings.
|
||||
struct PreviewSubscription {
|
||||
uint32_t request_id = 0;
|
||||
int fps =
|
||||
0; // 0 pauses encoding/delivery, 1 overview, 8 focus, 60 interactive.
|
||||
int quality = 0;
|
||||
int frame_rate = 30;
|
||||
int preference = 0;
|
||||
bool audio = false;
|
||||
|
||||
static PreviewSubscription Interactive(int quality, int frame_rate,
|
||||
int preference, bool audio = false) {
|
||||
return {0, 60, quality, frame_rate, preference, audio};
|
||||
}
|
||||
|
||||
bool SameSettings(const PreviewSubscription &other) const {
|
||||
return fps == other.fps && quality == other.quality &&
|
||||
frame_rate == other.frame_rate &&
|
||||
EncoderPreference() == other.EncoderPreference() &&
|
||||
audio == other.audio;
|
||||
}
|
||||
|
||||
int EncoderPreference() const {
|
||||
// Sparse preview input is intentional. Preserve spatial detail instead of
|
||||
// applying the interactive frame-rate/keyframe latency budget to it.
|
||||
return fps == 1 || fps == 8 ? 1 : preference;
|
||||
}
|
||||
|
||||
nlohmann::json ToJson() const {
|
||||
return {{"type", "deskhub_preview"}, {"version", 1},
|
||||
{"request_id", request_id}, {"fps", fps},
|
||||
{"quality", quality}, {"frame_rate", frame_rate},
|
||||
{"preference", EncoderPreference()}, {"audio", audio}};
|
||||
}
|
||||
static std::optional<PreviewSubscription> Parse(const nlohmann::json &json) {
|
||||
try {
|
||||
if (json.at("type") != "deskhub_preview" || json.at("version") != 1)
|
||||
return {};
|
||||
for (const char *key :
|
||||
{"request_id", "fps", "quality", "frame_rate", "preference"})
|
||||
if (!json.at(key).is_number_integer())
|
||||
return {};
|
||||
const auto id = json.at("request_id").get<int64_t>();
|
||||
if (id <= 0 || id > UINT32_MAX)
|
||||
return {};
|
||||
for (const char *key : {"fps", "quality", "frame_rate", "preference"}) {
|
||||
const auto value = json.at(key).get<int64_t>();
|
||||
if (value < 0 || value > 60)
|
||||
return {};
|
||||
}
|
||||
PreviewSubscription result{static_cast<uint32_t>(id), json.at("fps"),
|
||||
json.at("quality"), json.at("frame_rate"),
|
||||
json.at("preference")};
|
||||
if ((result.fps != 0 && result.fps != 1 && result.fps != 8 &&
|
||||
result.fps != 60) ||
|
||||
result.quality < 0 || result.quality > 2 ||
|
||||
(result.frame_rate != 30 && result.frame_rate != 60) ||
|
||||
result.preference < 0 || result.preference > 2)
|
||||
return {};
|
||||
if (json.contains("audio")) {
|
||||
if (!json.at("audio").is_boolean())
|
||||
return {};
|
||||
result.audio = json.at("audio").get<bool>();
|
||||
}
|
||||
return result;
|
||||
} catch (...) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -137,6 +137,7 @@ struct HostInfo {
|
||||
// Optional capability; absent in legacy desktop and web host information.
|
||||
bool supports_privacy_screen;
|
||||
bool supports_video_settings;
|
||||
bool supports_deskhub_preview;
|
||||
};
|
||||
|
||||
struct ServiceStatus {
|
||||
|
||||
@@ -26,6 +26,7 @@ void ResetHostInfo(HostInfo& info) {
|
||||
info.bottom = nullptr;
|
||||
info.supports_privacy_screen = false;
|
||||
info.supports_video_settings = false;
|
||||
info.supports_deskhub_preview = false;
|
||||
}
|
||||
|
||||
bool AllocateHostDisplays(HostInfo& info, std::size_t count) {
|
||||
@@ -219,7 +220,8 @@ std::string RemoteAction::ToJson(const RemoteAction& action) {
|
||||
{"display_num", action.i.display_num},
|
||||
{"displays", displays},
|
||||
{"supports_privacy_screen", action.i.supports_privacy_screen},
|
||||
{"supports_video_settings", action.i.supports_video_settings}};
|
||||
{"supports_video_settings", action.i.supports_video_settings},
|
||||
{"supports_deskhub_preview", action.i.supports_deskhub_preview}};
|
||||
break;
|
||||
}
|
||||
case ControlType::invalid:
|
||||
@@ -412,6 +414,8 @@ bool RemoteAction::FromJson(const std::string& json_string,
|
||||
privacy_support->is_boolean() && privacy_support->get<bool>();
|
||||
output.i.supports_video_settings =
|
||||
host_info_object.value("supports_video_settings", false);
|
||||
output.i.supports_deskhub_preview =
|
||||
host_info_object.value("supports_deskhub_preview", false);
|
||||
const std::string host_name =
|
||||
host_info_object.at("host_name").get<std::string>();
|
||||
std::strncpy(output.i.host_name, host_name.c_str(),
|
||||
|
||||
Reference in New Issue
Block a user