mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-03-22 07:37:29 +08:00
Compare commits
13 Commits
e4d530d044
...
v1.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e8ce6a2f0 | ||
|
|
9927a56b78 | ||
|
|
db3da52f83 | ||
|
|
19a7c6978a | ||
|
|
b5e9ba03a1 | ||
|
|
cb5f8b91ad | ||
|
|
f627f60f1a | ||
|
|
e9fce5b8b8 | ||
|
|
a7820a79db | ||
|
|
b6a52dbcd4 | ||
|
|
7bbd10a50c | ||
|
|
ee08b231db | ||
|
|
619e54dc0e |
25
src/common/window_util_mac.h
Normal file
25
src/common/window_util_mac.h
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-01-20
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOW_UTIL_MAC_H_
|
||||
#define _WINDOW_UTIL_MAC_H_
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Best-effort: keep an SDL window above normal windows on macOS.
|
||||
// No-op on non-macOS builds.
|
||||
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top);
|
||||
|
||||
// Best-effort: exclude an SDL window from the Window menu and window cycling.
|
||||
// Note: Cmd-Tab switches apps (not individual windows), so this primarily
|
||||
// affects the Window menu and Cmd-` window cycling.
|
||||
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
64
src/common/window_util_mac.mm
Normal file
64
src/common/window_util_mac.mm
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "window_util_mac.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static NSWindow* GetNSWindowFromSDL(::SDL_Window* window) {
|
||||
if (!window) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
#if !defined(SDL_PROP_WINDOW_COCOA_WINDOW_POINTER)
|
||||
return nil;
|
||||
#else
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
void* cocoa_window_ptr =
|
||||
SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL);
|
||||
if (!cocoa_window_ptr) {
|
||||
return nil;
|
||||
}
|
||||
return (__bridge NSWindow*)cocoa_window_ptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top) {
|
||||
NSWindow* ns_window = GetNSWindowFromSDL(window);
|
||||
if (!ns_window) {
|
||||
(void)always_on_top;
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep above normal windows.
|
||||
const NSInteger level = always_on_top ? NSFloatingWindowLevel : NSNormalWindowLevel;
|
||||
[ns_window setLevel:level];
|
||||
|
||||
// Optional: keep visible across Spaces/fullscreen. Safe as best-effort.
|
||||
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
|
||||
behavior |= NSWindowCollectionBehaviorCanJoinAllSpaces;
|
||||
behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
|
||||
[ns_window setCollectionBehavior:behavior];
|
||||
}
|
||||
|
||||
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded) {
|
||||
NSWindow* ns_window = GetNSWindowFromSDL(window);
|
||||
if (!ns_window) {
|
||||
(void)excluded;
|
||||
return;
|
||||
}
|
||||
|
||||
[ns_window setExcludedFromWindowsMenu:excluded];
|
||||
|
||||
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
|
||||
behavior |= NSWindowCollectionBehaviorIgnoresCycle;
|
||||
behavior |= NSWindowCollectionBehaviorTransient;
|
||||
[ns_window setCollectionBehavior:behavior];
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // __APPLE__
|
||||
File diff suppressed because it is too large
Load Diff
@@ -197,6 +197,12 @@ static std::vector<std::string> completed = {
|
||||
reinterpret_cast<const char*>(u8"已完成"), "Completed"};
|
||||
static std::vector<std::string> failed = {
|
||||
reinterpret_cast<const char*>(u8"失败"), "Failed"};
|
||||
static std::vector<std::string> controller = {
|
||||
reinterpret_cast<const char*>(u8"控制端:"), "Controller:"};
|
||||
static std::vector<std::string> file_transfer = {
|
||||
reinterpret_cast<const char*>(u8"文件传输:"), "File Transfer:"};
|
||||
static std::vector<std::string> connection_status = {
|
||||
reinterpret_cast<const char*>(u8"连接状态:"), "Connection Status:"};
|
||||
|
||||
#if _WIN32
|
||||
static std::vector<std::string> minimize_to_tray = {
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
#include <libyuv.h>
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -21,10 +27,123 @@
|
||||
#include "screen_capturer_factory.h"
|
||||
#include "version_checker.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include "window_util_mac.h"
|
||||
#endif
|
||||
|
||||
#define NV12_BUFFER_SIZE 1280 * 720 * 3 / 2
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
inline bool X11GetDisplayAndWindow(SDL_Window* window, Display** display_out,
|
||||
::Window* x11_window_out) {
|
||||
if (!window || !display_out || !x11_window_out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if !defined(SDL_PROP_WINDOW_X11_DISPLAY_POINTER) || \
|
||||
!defined(SDL_PROP_WINDOW_X11_WINDOW_NUMBER)
|
||||
// SDL build does not expose X11 window properties.
|
||||
return false;
|
||||
#else
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
Display* display = (Display*)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL);
|
||||
const Sint64 x11_window_num =
|
||||
SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
|
||||
const ::Window x11_window = (::Window)x11_window_num;
|
||||
|
||||
if (!display || !x11_window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*display_out = display;
|
||||
*x11_window_out = x11_window;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void X11SendNetWmState(Display* display, ::Window x11_window,
|
||||
long action, Atom state1, Atom state2 = 0) {
|
||||
if (!display || !x11_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom wm_state = XInternAtom(display, "_NET_WM_STATE", False);
|
||||
|
||||
XEvent event;
|
||||
memset(&event, 0, sizeof(event));
|
||||
event.xclient.type = ClientMessage;
|
||||
event.xclient.serial = 0;
|
||||
event.xclient.send_event = True;
|
||||
event.xclient.message_type = wm_state;
|
||||
event.xclient.window = x11_window;
|
||||
event.xclient.format = 32;
|
||||
event.xclient.data.l[0] = action;
|
||||
event.xclient.data.l[1] = (long)state1;
|
||||
event.xclient.data.l[2] = (long)state2;
|
||||
event.xclient.data.l[3] = 1; // normal source indication
|
||||
event.xclient.data.l[4] = 0;
|
||||
|
||||
XSendEvent(display, DefaultRootWindow(display), False,
|
||||
SubstructureRedirectMask | SubstructureNotifyMask, &event);
|
||||
}
|
||||
|
||||
inline void X11SetWindowTypeUtility(Display* display, ::Window x11_window) {
|
||||
if (!display || !x11_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom wm_window_type =
|
||||
XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
|
||||
const Atom wm_window_type_utility =
|
||||
XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
|
||||
|
||||
XChangeProperty(display, x11_window, wm_window_type, XA_ATOM, 32,
|
||||
PropModeReplace, (unsigned char*)&wm_window_type_utility, 1);
|
||||
}
|
||||
|
||||
inline void X11SetWindowAlwaysOnTop(SDL_Window* window) {
|
||||
Display* display = nullptr;
|
||||
::Window x11_window = 0;
|
||||
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom state_above = XInternAtom(display, "_NET_WM_STATE_ABOVE", False);
|
||||
const Atom state_stays_on_top =
|
||||
XInternAtom(display, "_NET_WM_STATE_STAYS_ON_TOP", False);
|
||||
|
||||
// Request _NET_WM_STATE_ADD for ABOVE + STAYS_ON_TOP.
|
||||
X11SendNetWmState(display, x11_window, 1, state_above, state_stays_on_top);
|
||||
XFlush(display);
|
||||
}
|
||||
|
||||
inline void X11SetWindowSkipTaskbar(SDL_Window* window) {
|
||||
Display* display = nullptr;
|
||||
::Window x11_window = 0;
|
||||
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom skip_taskbar =
|
||||
XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", False);
|
||||
const Atom skip_pager =
|
||||
XInternAtom(display, "_NET_WM_STATE_SKIP_PAGER", False);
|
||||
|
||||
// Request _NET_WM_STATE_ADD for SKIP_TASKBAR + SKIP_PAGER.
|
||||
X11SendNetWmState(display, x11_window, 1, skip_taskbar, skip_pager);
|
||||
|
||||
// Hint the WM that this is an auxiliary/utility window.
|
||||
X11SetWindowTypeUtility(display, x11_window);
|
||||
|
||||
XFlush(display);
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
std::vector<char> Render::SerializeRemoteAction(const RemoteAction& action) {
|
||||
std::vector<char> buffer;
|
||||
buffer.push_back(static_cast<char>(action.type));
|
||||
@@ -131,6 +250,20 @@ SDL_HitTestResult Render::HitTestCallback(SDL_Window* window,
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
// Server window: OS-level dragging for the title bar, but keep the left-side
|
||||
// collapse/expand button clickable.
|
||||
if (render->server_window_ && window == render->server_window_) {
|
||||
const float title_h = render->server_window_title_bar_height_;
|
||||
const float button_w = title_h;
|
||||
if (area->y >= 0 && area->y < title_h) {
|
||||
if (area->x >= 0 && area->x < button_w) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
return SDL_HITTEST_DRAGGABLE;
|
||||
}
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
int window_width, window_height;
|
||||
SDL_GetWindowSize(window, &window_width, &window_height);
|
||||
|
||||
@@ -696,7 +829,7 @@ int Render::CreateConnectionPeer() {
|
||||
|
||||
params_.on_signal_status = OnSignalStatusCb;
|
||||
params_.on_connection_status = OnConnectionStatusCb;
|
||||
params_.net_status_report = NetStatusReport;
|
||||
params_.on_net_status_report = OnNetStatusReport;
|
||||
|
||||
params_.user_data = this;
|
||||
|
||||
@@ -815,6 +948,12 @@ int Render::CreateMainWindow() {
|
||||
main_window_height_ = (int)(main_window_height_default_ * dpi_scale_);
|
||||
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
|
||||
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
|
||||
server_window_width_ = (int)(server_window_width_default_ * dpi_scale_);
|
||||
server_window_height_ = (int)(server_window_height_default_ * dpi_scale_);
|
||||
server_window_normal_width_ =
|
||||
(int)(server_window_width_default_ * dpi_scale_);
|
||||
server_window_normal_height_ =
|
||||
(int)(server_window_height_default_ * dpi_scale_);
|
||||
|
||||
SDL_SetWindowSize(main_window_, (int)main_window_width_,
|
||||
(int)main_window_height_);
|
||||
@@ -963,6 +1102,40 @@ int Render::CreateServerWindow() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
// Hide server window from the taskbar by making it a tool window.
|
||||
{
|
||||
SDL_PropertiesID server_props = SDL_GetWindowProperties(server_window_);
|
||||
HWND server_hwnd = (HWND)SDL_GetPointerProperty(
|
||||
server_props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
|
||||
|
||||
if (server_hwnd) {
|
||||
LONG_PTR ex_style = GetWindowLongPtr(server_hwnd, GWL_EXSTYLE);
|
||||
ex_style |= WS_EX_TOOLWINDOW;
|
||||
ex_style &= ~WS_EX_APPWINDOW;
|
||||
SetWindowLongPtr(server_hwnd, GWL_EXSTYLE, ex_style);
|
||||
|
||||
// Keep the server window above normal windows.
|
||||
SetWindowPos(server_hwnd, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOACTIVATE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
// Best-effort keep above other windows on X11.
|
||||
X11SetWindowAlwaysOnTop(server_window_);
|
||||
// Best-effort hide from taskbar on X11.
|
||||
X11SetWindowSkipTaskbar(server_window_);
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// Best-effort keep above other windows on macOS.
|
||||
MacSetWindowAlwaysOnTop(server_window_, true);
|
||||
// Best-effort exclude from Window menu / window cycling.
|
||||
MacSetWindowExcludedFromWindowMenu(server_window_, true);
|
||||
#endif
|
||||
|
||||
// Set window position to bottom-right corner
|
||||
SDL_Rect display_bounds;
|
||||
if (SDL_GetDisplayUsableBounds(SDL_GetDisplayForWindow(server_window_),
|
||||
@@ -1448,49 +1621,6 @@ void Render::MainLoop() {
|
||||
}
|
||||
|
||||
UpdateInteractions();
|
||||
|
||||
if (need_to_send_host_info_) {
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num = display_info_list_.size();
|
||||
remote_action.i.display_list =
|
||||
(char**)malloc(remote_action.i.display_num * sizeof(char*));
|
||||
remote_action.i.left =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.top =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.right =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.bottom =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1, display_info_list_[i].name);
|
||||
remote_action.i.display_list[i] =
|
||||
(char*)malloc(display_info_list_[i].name.length() + 1);
|
||||
strncpy(remote_action.i.display_list[i],
|
||||
display_info_list_[i].name.c_str(),
|
||||
display_info_list_[i].name.length());
|
||||
remote_action.i.display_list[i][display_info_list_[i].name.length()] =
|
||||
'\0';
|
||||
remote_action.i.left[i] = display_info_list_[i].left;
|
||||
remote_action.i.top[i] = display_info_list_[i].top;
|
||||
remote_action.i.right[i] = display_info_list_[i].right;
|
||||
remote_action.i.bottom[i] = display_info_list_[i].bottom;
|
||||
}
|
||||
|
||||
std::string host_name = GetHostName();
|
||||
remote_action.type = ControlType::host_infomation;
|
||||
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
|
||||
remote_action.i.host_name[host_name.size()] = '\0';
|
||||
remote_action.i.host_name_size = host_name.size();
|
||||
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(),
|
||||
control_data_label_.c_str());
|
||||
FreeRemoteAction(remote_action);
|
||||
if (0 == ret) {
|
||||
need_to_send_host_info_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1607,13 +1737,19 @@ void Render::CleanupFactories() {
|
||||
void Render::CleanupPeer(std::shared_ptr<SubStreamWindowProperties> props) {
|
||||
SDL_FlushEvent(STREAM_REFRESH_EVENT);
|
||||
|
||||
if (props->dst_buffer_) {
|
||||
size_t buffer_size = props->dst_buffer_capacity_;
|
||||
std::vector<unsigned char> buffer_copy(buffer_size);
|
||||
memcpy(buffer_copy.data(), props->dst_buffer_, buffer_size);
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
frame_snapshot = props->front_frame_;
|
||||
video_width = props->video_width_;
|
||||
video_height = props->video_height_;
|
||||
}
|
||||
|
||||
int video_width = props->video_width_;
|
||||
int video_height = props->video_height_;
|
||||
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 =
|
||||
@@ -1694,22 +1830,47 @@ void Render::CleanSubStreamWindowProperties(
|
||||
props->stream_texture_ = nullptr;
|
||||
}
|
||||
|
||||
if (props->dst_buffer_) {
|
||||
delete[] props->dst_buffer_;
|
||||
props->dst_buffer_ = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
props->video_width_ = 0;
|
||||
props->video_height_ = 0;
|
||||
props->video_size_ = 0;
|
||||
props->render_rect_dirty_ = true;
|
||||
props->stream_cleanup_pending_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Render::SubStreamWindowProperties>
|
||||
Render::GetSubStreamWindowPropertiesByRemoteId(const std::string& remote_id) {
|
||||
if (remote_id.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_lock lock(client_properties_mutex_);
|
||||
auto it = client_properties_.find(remote_id);
|
||||
if (it == client_properties_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
void Render::StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
|
||||
const std::filesystem::path& file_path,
|
||||
const std::string& file_label) {
|
||||
if (!props || !props->peer_) {
|
||||
LOG_ERROR("StartFileTransfer: invalid props or peer");
|
||||
const std::string& file_label,
|
||||
const std::string& remote_id) {
|
||||
const bool is_global = (props == nullptr);
|
||||
PeerPtr* peer = is_global ? peer_ : props->peer_;
|
||||
if (!peer) {
|
||||
LOG_ERROR("StartFileTransfer: invalid peer");
|
||||
return;
|
||||
}
|
||||
|
||||
bool expected = false;
|
||||
if (!props->file_sending_.compare_exchange_strong(expected, true)) {
|
||||
if (!(is_global ? file_transfer_.file_sending_
|
||||
: props->file_transfer_.file_sending_)
|
||||
.compare_exchange_strong(expected, true)) {
|
||||
// Already sending, this should not happen if called correctly
|
||||
LOG_WARN(
|
||||
"StartFileTransfer called but file_sending_ is already true, "
|
||||
@@ -1718,13 +1879,19 @@ void Render::StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
|
||||
return;
|
||||
}
|
||||
|
||||
auto peer = props->peer_;
|
||||
auto props_weak = std::weak_ptr<SubStreamWindowProperties>(props);
|
||||
Render* render_ptr = this;
|
||||
|
||||
std::thread([peer, file_path, file_label, props_weak, render_ptr]() {
|
||||
std::thread([peer, file_path, file_label, props_weak, render_ptr, remote_id,
|
||||
is_global]() {
|
||||
auto props_locked = props_weak.lock();
|
||||
if (!props_locked) {
|
||||
|
||||
FileTransferState* state = nullptr;
|
||||
if (props_locked) {
|
||||
state = &props_locked->file_transfer_;
|
||||
} else if (is_global) {
|
||||
state = &render_ptr->file_transfer_;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1732,49 +1899,50 @@ void Render::StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
|
||||
uint64_t total_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
|
||||
props_locked->file_sending_ = false;
|
||||
state->file_sending_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
props_locked->file_sent_bytes_ = 0;
|
||||
props_locked->file_total_bytes_ = total_size;
|
||||
props_locked->file_send_rate_bps_ = 0;
|
||||
props_locked->file_transfer_window_visible_ = true;
|
||||
state->file_sent_bytes_ = 0;
|
||||
state->file_total_bytes_ = total_size;
|
||||
state->file_send_rate_bps_ = 0;
|
||||
state->file_transfer_window_visible_ = true;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props_locked->file_transfer_mutex_);
|
||||
props_locked->file_send_start_time_ = std::chrono::steady_clock::now();
|
||||
props_locked->file_send_last_update_time_ =
|
||||
props_locked->file_send_start_time_;
|
||||
props_locked->file_send_last_bytes_ = 0;
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
|
||||
state->file_send_start_time_ = std::chrono::steady_clock::now();
|
||||
state->file_send_last_update_time_ = state->file_send_start_time_;
|
||||
state->file_send_last_bytes_ = 0;
|
||||
}
|
||||
|
||||
LOG_INFO(
|
||||
"File transfer started: {} ({} bytes), file_sending_={}, "
|
||||
"total_bytes_={}",
|
||||
file_path.filename().string(), total_size,
|
||||
props_locked->file_sending_.load(),
|
||||
props_locked->file_total_bytes_.load());
|
||||
file_path.filename().string(), total_size, state->file_sending_.load(),
|
||||
state->file_total_bytes_.load());
|
||||
|
||||
FileSender sender;
|
||||
uint32_t file_id = FileSender::NextFileId();
|
||||
|
||||
{
|
||||
if (props_locked) {
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render_ptr->file_id_to_props_mutex_);
|
||||
render_ptr->file_id_to_props_[file_id] = props_weak;
|
||||
} else {
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render_ptr->file_id_to_transfer_state_mutex_);
|
||||
render_ptr->file_id_to_transfer_state_[file_id] = state;
|
||||
}
|
||||
|
||||
props_locked->current_file_id_ = file_id;
|
||||
state->current_file_id_ = file_id;
|
||||
|
||||
// Update file transfer list: mark as sending
|
||||
// Find the queued file that matches the exact file path
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props_locked->file_transfer_list_mutex_);
|
||||
for (auto& info : props_locked->file_transfer_list_) {
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto& info : state->file_transfer_list_) {
|
||||
if (info.file_path == file_path &&
|
||||
info.status ==
|
||||
SubStreamWindowProperties::FileTransferStatus::Queued) {
|
||||
info.status = SubStreamWindowProperties::FileTransferStatus::Sending;
|
||||
info.status == FileTransferState::FileTransferStatus::Queued) {
|
||||
info.status = FileTransferState::FileTransferStatus::Sending;
|
||||
info.file_id = file_id;
|
||||
info.file_size = total_size;
|
||||
info.sent_bytes = 0;
|
||||
@@ -1783,81 +1951,88 @@ void Render::StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
|
||||
}
|
||||
}
|
||||
|
||||
props_locked->file_transfer_window_visible_ = true;
|
||||
state->file_transfer_window_visible_ = true;
|
||||
|
||||
// Progress will be updated via ACK from receiver
|
||||
int ret = sender.SendFile(
|
||||
file_path, file_path.filename().string(),
|
||||
[peer, file_label](const char* buf, size_t sz) -> int {
|
||||
return SendReliableDataFrame(peer, buf, sz, file_label.c_str());
|
||||
[peer, file_label, remote_id](const char* buf, size_t sz) -> int {
|
||||
if (remote_id.empty()) {
|
||||
return SendReliableDataFrame(peer, buf, sz, file_label.c_str());
|
||||
} else {
|
||||
return SendReliableDataFrameToPeer(
|
||||
peer, buf, sz, file_label.c_str(), remote_id.c_str(),
|
||||
remote_id.size());
|
||||
}
|
||||
},
|
||||
64 * 1024, file_id);
|
||||
|
||||
// file_sending_ should remain true until we receive the final ACK from
|
||||
// receiver
|
||||
auto props_locked_final = props_weak.lock();
|
||||
if (props_locked_final) {
|
||||
// On error, set file_sending_ to false immediately to allow next file
|
||||
if (ret != 0) {
|
||||
props_locked_final->file_sending_ = false;
|
||||
props_locked_final->file_transfer_window_visible_ = false;
|
||||
props_locked_final->file_sent_bytes_ = 0;
|
||||
props_locked_final->file_total_bytes_ = 0;
|
||||
props_locked_final->file_send_rate_bps_ = 0;
|
||||
props_locked_final->current_file_id_ = 0;
|
||||
// On error, set file_sending_ to false immediately to allow next file
|
||||
if (ret != 0) {
|
||||
state->file_sending_ = false;
|
||||
state->file_transfer_window_visible_ = false;
|
||||
state->file_sent_bytes_ = 0;
|
||||
state->file_total_bytes_ = 0;
|
||||
state->file_send_rate_bps_ = 0;
|
||||
state->current_file_id_ = 0;
|
||||
|
||||
// Unregister file_id mapping on error
|
||||
{
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render_ptr->file_id_to_props_mutex_);
|
||||
render_ptr->file_id_to_props_.erase(file_id);
|
||||
}
|
||||
// Unregister file_id mapping on error
|
||||
if (props_locked) {
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render_ptr->file_id_to_props_mutex_);
|
||||
render_ptr->file_id_to_props_.erase(file_id);
|
||||
} else {
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render_ptr->file_id_to_transfer_state_mutex_);
|
||||
render_ptr->file_id_to_transfer_state_.erase(file_id);
|
||||
}
|
||||
|
||||
// Update file transfer list: mark as failed
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
props_locked_final->file_transfer_list_mutex_);
|
||||
for (auto& info : props_locked_final->file_transfer_list_) {
|
||||
if (info.file_id == file_id) {
|
||||
info.status =
|
||||
SubStreamWindowProperties::FileTransferStatus::Failed;
|
||||
break;
|
||||
}
|
||||
// Update file transfer list: mark as failed
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto& info : state->file_transfer_list_) {
|
||||
if (info.file_id == file_id) {
|
||||
info.status = FileTransferState::FileTransferStatus::Failed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
|
||||
file_path.string().c_str(), ret);
|
||||
|
||||
render_ptr->ProcessFileQueue(props_locked_final);
|
||||
}
|
||||
|
||||
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
|
||||
file_path.string().c_str(), ret);
|
||||
|
||||
render_ptr->ProcessFileQueue(props_locked);
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void Render::ProcessFileQueue(
|
||||
std::shared_ptr<SubStreamWindowProperties> props) {
|
||||
if (!props) {
|
||||
FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_;
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (props->file_sending_.load()) {
|
||||
if (state->file_sending_.load()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubStreamWindowProperties::QueuedFile queued_file;
|
||||
FileTransferState::QueuedFile queued_file;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_queue_mutex_);
|
||||
if (props->file_send_queue_.empty()) {
|
||||
std::lock_guard<std::mutex> lock(state->file_queue_mutex_);
|
||||
if (state->file_send_queue_.empty()) {
|
||||
return;
|
||||
}
|
||||
queued_file = props->file_send_queue_.front();
|
||||
props->file_send_queue_.pop();
|
||||
queued_file = state->file_send_queue_.front();
|
||||
state->file_send_queue_.pop();
|
||||
}
|
||||
|
||||
LOG_INFO("Processing next file in queue: {}",
|
||||
queued_file.file_path.string().c_str());
|
||||
StartFileTransfer(props, queued_file.file_path, queued_file.file_label);
|
||||
StartFileTransfer(props, queued_file.file_path, queued_file.file_label,
|
||||
queued_file.remote_id);
|
||||
}
|
||||
|
||||
void Render::UpdateRenderRect() {
|
||||
@@ -1949,10 +2124,22 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
|
||||
{
|
||||
// std::shared_lock lock(client_properties_mutex_);
|
||||
for (auto& [host_name, props] : client_properties_) {
|
||||
thumbnail_->SaveToThumbnail(
|
||||
(char*)props->dst_buffer_, props->video_width_,
|
||||
props->video_height_, host_name, props->remote_host_name_,
|
||||
props->remember_password_ ? props->remote_password_ : "");
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
frame_snapshot = props->front_frame_;
|
||||
video_width = props->video_width_;
|
||||
video_height = props->video_height_;
|
||||
}
|
||||
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
||||
video_height > 0) {
|
||||
thumbnail_->SaveToThumbnail(
|
||||
(char*)frame_snapshot->data(), video_width, video_height,
|
||||
host_name, props->remote_host_name_,
|
||||
props->remember_password_ ? props->remote_password_ : "");
|
||||
}
|
||||
|
||||
if (props->peer_) {
|
||||
std::string client_id = (host_name == client_id_)
|
||||
@@ -2028,10 +2215,7 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_DROP_FILE:
|
||||
if (stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == event.window.windowID) {
|
||||
ProcessFileDropEvent(event);
|
||||
}
|
||||
ProcessFileDropEvent(event);
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
@@ -2049,18 +2233,52 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
|
||||
if (!props) {
|
||||
break;
|
||||
}
|
||||
if (props->video_width_ <= 0 || props->video_height_ <= 0) {
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
bool render_rect_dirty = false;
|
||||
bool cleanup_pending = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
cleanup_pending = props->stream_cleanup_pending_;
|
||||
if (!cleanup_pending) {
|
||||
frame_snapshot = props->front_frame_;
|
||||
video_width = props->video_width_;
|
||||
video_height = props->video_height_;
|
||||
}
|
||||
render_rect_dirty = props->render_rect_dirty_;
|
||||
}
|
||||
|
||||
if (cleanup_pending) {
|
||||
if (props->stream_texture_) {
|
||||
SDL_DestroyTexture(props->stream_texture_);
|
||||
props->stream_texture_ = nullptr;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->stream_cleanup_pending_ = false;
|
||||
}
|
||||
|
||||
if (render_rect_dirty) {
|
||||
UpdateRenderRect();
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->render_rect_dirty_ = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!props->dst_buffer_) {
|
||||
|
||||
if (video_width <= 0 || video_height <= 0) {
|
||||
break;
|
||||
}
|
||||
if (!frame_snapshot || frame_snapshot->empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (props->stream_texture_) {
|
||||
if (props->video_width_ != props->texture_width_ ||
|
||||
props->video_height_ != props->texture_height_) {
|
||||
props->texture_width_ = props->video_width_;
|
||||
props->texture_height_ = props->video_height_;
|
||||
if (video_width != props->texture_width_ ||
|
||||
video_height != props->texture_height_) {
|
||||
props->texture_width_ = video_width;
|
||||
props->texture_height_ = video_height;
|
||||
|
||||
SDL_DestroyTexture(props->stream_texture_);
|
||||
// props->stream_texture_ = SDL_CreateTexture(
|
||||
@@ -2085,8 +2303,8 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
|
||||
SDL_DestroyProperties(nvProps);
|
||||
}
|
||||
} else {
|
||||
props->texture_width_ = props->video_width_;
|
||||
props->texture_height_ = props->video_height_;
|
||||
props->texture_width_ = video_width;
|
||||
props->texture_height_ = video_height;
|
||||
// props->stream_texture_ = SDL_CreateTexture(
|
||||
// stream_renderer_, stream_pixformat_,
|
||||
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
|
||||
@@ -2107,69 +2325,124 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
|
||||
SDL_DestroyProperties(nvProps);
|
||||
}
|
||||
|
||||
SDL_UpdateTexture(props->stream_texture_, NULL, props->dst_buffer_,
|
||||
SDL_UpdateTexture(props->stream_texture_, NULL, frame_snapshot->data(),
|
||||
props->texture_width_);
|
||||
|
||||
if (render_rect_dirty) {
|
||||
UpdateRenderRect();
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->render_rect_dirty_ = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Render::ProcessFileDropEvent(const SDL_Event& event) {
|
||||
if (!((stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == event.window.windowID) ||
|
||||
(server_window_ &&
|
||||
SDL_GetWindowID(server_window_) == event.window.windowID))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type != SDL_EVENT_DROP_FILE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stream_window_inited_) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_lock lock(client_properties_mutex_);
|
||||
for (auto& [_, props] : client_properties_) {
|
||||
if (props->tab_selected_) {
|
||||
if (event.drop.data == nullptr) {
|
||||
LOG_ERROR("ProcessFileDropEvent: drop event data is null");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!props || !props->peer_) {
|
||||
LOG_ERROR("ProcessFileDropEvent: invalid props or peer");
|
||||
break;
|
||||
}
|
||||
|
||||
std::string utf8_path = static_cast<const char*>(event.drop.data);
|
||||
std::filesystem::path file_path = std::filesystem::u8path(utf8_path);
|
||||
|
||||
// Check if file exists
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(file_path, ec)) {
|
||||
LOG_ERROR("ProcessFileDropEvent: file does not exist: {}",
|
||||
file_path.string().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if it's a regular file
|
||||
if (!std::filesystem::is_regular_file(file_path, ec)) {
|
||||
LOG_ERROR("ProcessFileDropEvent: path is not a regular file: {}",
|
||||
file_path.string().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
// Get file size
|
||||
uint64_t file_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("ProcessFileDropEvent: failed to get file size: {}",
|
||||
ec.message().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_INFO("Drop file [{}] to send (size: {} bytes)", event.drop.data,
|
||||
file_size);
|
||||
|
||||
// Use ProcessSelectedFile to handle the file processing
|
||||
ProcessSelectedFile(utf8_path, props, props->file_label_);
|
||||
|
||||
break;
|
||||
if (SDL_GetWindowID(stream_window_) == event.window.windowID) {
|
||||
if (!stream_window_inited_) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_lock lock(client_properties_mutex_);
|
||||
for (auto& [_, props] : client_properties_) {
|
||||
if (props->tab_selected_) {
|
||||
if (event.drop.data == nullptr) {
|
||||
LOG_ERROR("ProcessFileDropEvent: drop event data is null");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!props || !props->peer_) {
|
||||
LOG_ERROR("ProcessFileDropEvent: invalid props or peer");
|
||||
break;
|
||||
}
|
||||
|
||||
std::string utf8_path = static_cast<const char*>(event.drop.data);
|
||||
std::filesystem::path file_path = std::filesystem::u8path(utf8_path);
|
||||
|
||||
// Check if file exists
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(file_path, ec)) {
|
||||
LOG_ERROR("ProcessFileDropEvent: file does not exist: {}",
|
||||
file_path.string().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if it's a regular file
|
||||
if (!std::filesystem::is_regular_file(file_path, ec)) {
|
||||
LOG_ERROR("ProcessFileDropEvent: path is not a regular file: {}",
|
||||
file_path.string().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
// Get file size
|
||||
uint64_t file_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("ProcessFileDropEvent: failed to get file size: {}",
|
||||
ec.message().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_INFO("Drop file [{}] to send (size: {} bytes)", event.drop.data,
|
||||
file_size);
|
||||
|
||||
// Use ProcessSelectedFile to handle the file processing
|
||||
ProcessSelectedFile(utf8_path, props, props->file_label_);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (SDL_GetWindowID(server_window_) == event.window.windowID) {
|
||||
if (!server_window_inited_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.drop.data == nullptr) {
|
||||
LOG_ERROR("ProcessFileDropEvent: drop event data is null");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string utf8_path = static_cast<const char*>(event.drop.data);
|
||||
std::filesystem::path file_path = std::filesystem::u8path(utf8_path);
|
||||
|
||||
// Check if file exists
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(file_path, ec)) {
|
||||
LOG_ERROR("ProcessFileDropEvent: file does not exist: {}",
|
||||
file_path.string().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a regular file
|
||||
if (!std::filesystem::is_regular_file(file_path, ec)) {
|
||||
LOG_ERROR("ProcessFileDropEvent: path is not a regular file: {}",
|
||||
file_path.string().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get file size
|
||||
uint64_t file_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("ProcessFileDropEvent: failed to get file size: {}",
|
||||
ec.message().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Drop file [{}] on server window (size: {} bytes)",
|
||||
event.drop.data, file_size);
|
||||
|
||||
// Handle the dropped file on server window as needed
|
||||
}
|
||||
}
|
||||
} // namespace crossdesk
|
||||
136
src/gui/render.h
136
src/gui/render.h
@@ -42,6 +42,41 @@
|
||||
namespace crossdesk {
|
||||
class Render {
|
||||
public:
|
||||
struct FileTransferState {
|
||||
std::atomic<bool> file_sending_ = false;
|
||||
std::atomic<uint64_t> file_sent_bytes_ = 0;
|
||||
std::atomic<uint64_t> file_total_bytes_ = 0;
|
||||
std::atomic<uint32_t> file_send_rate_bps_ = 0;
|
||||
std::mutex file_transfer_mutex_;
|
||||
std::chrono::steady_clock::time_point file_send_start_time_;
|
||||
std::chrono::steady_clock::time_point file_send_last_update_time_;
|
||||
uint64_t file_send_last_bytes_ = 0;
|
||||
bool file_transfer_window_visible_ = false;
|
||||
std::atomic<uint32_t> current_file_id_{0};
|
||||
|
||||
struct QueuedFile {
|
||||
std::filesystem::path file_path;
|
||||
std::string file_label;
|
||||
std::string remote_id;
|
||||
};
|
||||
std::queue<QueuedFile> file_send_queue_;
|
||||
std::mutex file_queue_mutex_;
|
||||
|
||||
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
|
||||
|
||||
struct FileTransferInfo {
|
||||
std::string file_name;
|
||||
std::filesystem::path file_path;
|
||||
uint64_t file_size = 0;
|
||||
FileTransferStatus status = FileTransferStatus::Queued;
|
||||
uint64_t sent_bytes = 0;
|
||||
uint32_t file_id = 0;
|
||||
uint32_t rate_bps = 0;
|
||||
};
|
||||
std::vector<FileTransferInfo> file_transfer_list_;
|
||||
std::mutex file_transfer_list_mutex_;
|
||||
};
|
||||
|
||||
struct SubStreamWindowProperties {
|
||||
Params params_;
|
||||
PeerPtr* peer_ = nullptr;
|
||||
@@ -88,8 +123,13 @@ class Render {
|
||||
float mouse_diff_control_bar_pos_y_ = 0;
|
||||
double control_bar_button_pressed_time_ = 0;
|
||||
double net_traffic_stats_button_pressed_time_ = 0;
|
||||
unsigned char* dst_buffer_ = nullptr;
|
||||
size_t dst_buffer_capacity_ = 0;
|
||||
// Double-buffered NV12 frame storage. Written by decode callback thread,
|
||||
// consumed by SDL main thread.
|
||||
std::mutex video_frame_mutex_;
|
||||
std::shared_ptr<std::vector<unsigned char>> front_frame_;
|
||||
std::shared_ptr<std::vector<unsigned char>> back_frame_;
|
||||
bool render_rect_dirty_ = false;
|
||||
bool stream_cleanup_pending_ = false;
|
||||
float mouse_pos_x_ = 0;
|
||||
float mouse_pos_y_ = 0;
|
||||
float mouse_pos_x_last_ = 0;
|
||||
@@ -129,38 +169,10 @@ class Render {
|
||||
std::chrono::steady_clock::time_point last_time_;
|
||||
XNetTrafficStats net_traffic_stats_;
|
||||
|
||||
// File transfer progress
|
||||
std::atomic<bool> file_sending_ = false;
|
||||
std::atomic<uint64_t> file_sent_bytes_ = 0;
|
||||
std::atomic<uint64_t> file_total_bytes_ = 0;
|
||||
std::atomic<uint32_t> file_send_rate_bps_ = 0;
|
||||
std::mutex file_transfer_mutex_;
|
||||
std::chrono::steady_clock::time_point file_send_start_time_;
|
||||
std::chrono::steady_clock::time_point file_send_last_update_time_;
|
||||
uint64_t file_send_last_bytes_ = 0;
|
||||
bool file_transfer_window_visible_ = false;
|
||||
std::atomic<uint32_t> current_file_id_{0};
|
||||
|
||||
struct QueuedFile {
|
||||
std::filesystem::path file_path;
|
||||
std::string file_label;
|
||||
};
|
||||
std::queue<QueuedFile> file_send_queue_;
|
||||
std::mutex file_queue_mutex_;
|
||||
|
||||
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
|
||||
|
||||
struct FileTransferInfo {
|
||||
std::string file_name;
|
||||
std::filesystem::path file_path;
|
||||
uint64_t file_size = 0;
|
||||
FileTransferStatus status = FileTransferStatus::Queued;
|
||||
uint64_t sent_bytes = 0;
|
||||
uint32_t file_id = 0;
|
||||
uint32_t rate_bps = 0;
|
||||
};
|
||||
std::vector<FileTransferInfo> file_transfer_list_;
|
||||
std::mutex file_transfer_list_mutex_;
|
||||
using QueuedFile = FileTransferState::QueuedFile;
|
||||
using FileTransferStatus = FileTransferState::FileTransferStatus;
|
||||
using FileTransferInfo = FileTransferState::FileTransferInfo;
|
||||
FileTransferState file_transfer_;
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -193,9 +205,13 @@ class Render {
|
||||
|
||||
void ProcessFileDropEvent(const SDL_Event& event);
|
||||
|
||||
void ProcessSelectedFile(const std::string& path,
|
||||
std::shared_ptr<SubStreamWindowProperties>& props,
|
||||
const std::string& file_label);
|
||||
void ProcessSelectedFile(
|
||||
const std::string& path,
|
||||
const std::shared_ptr<SubStreamWindowProperties>& props,
|
||||
const std::string& file_label, const std::string& remote_id = "");
|
||||
|
||||
std::shared_ptr<SubStreamWindowProperties>
|
||||
GetSubStreamWindowPropertiesByRemoteId(const std::string& remote_id);
|
||||
|
||||
private:
|
||||
int CreateStreamRenderWindow();
|
||||
@@ -204,6 +220,7 @@ class Render {
|
||||
int UpdateNotificationWindow();
|
||||
int StreamWindow();
|
||||
int ServerWindow();
|
||||
int RemoteClientInfoWindow();
|
||||
int LocalWindow();
|
||||
int RemoteWindow();
|
||||
int RecentConnectionsWindow();
|
||||
@@ -220,6 +237,7 @@ class Render {
|
||||
void Hyperlink(const std::string& label, const std::string& url,
|
||||
const float window_width);
|
||||
int FileTransferWindow(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
std::string OpenFileDialog(std::string title);
|
||||
|
||||
private:
|
||||
int ConnectTo(const std::string& remote_id, const char* password,
|
||||
@@ -272,11 +290,11 @@ class Render {
|
||||
static void OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
size_t user_id_size, void* user_data);
|
||||
|
||||
static void NetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data);
|
||||
static void OnNetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data);
|
||||
|
||||
static SDL_HitTestResult HitTestCallback(SDL_Window* window,
|
||||
const SDL_Point* area, void* data);
|
||||
@@ -317,7 +335,8 @@ class Render {
|
||||
// File transfer helper functions
|
||||
void StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
|
||||
const std::filesystem::path& file_path,
|
||||
const std::string& file_label);
|
||||
const std::string& file_label,
|
||||
const std::string& remote_id = "");
|
||||
void ProcessFileQueue(std::shared_ptr<SubStreamWindowProperties> props);
|
||||
|
||||
int AudioDeviceInit();
|
||||
@@ -418,6 +437,9 @@ class Render {
|
||||
bool keyboard_capturer_is_started_ = false;
|
||||
bool foucs_on_main_window_ = false;
|
||||
bool foucs_on_stream_window_ = false;
|
||||
bool main_window_minimized_ = false;
|
||||
uint32_t last_main_minimize_request_tick_ = 0;
|
||||
uint32_t last_stream_minimize_request_tick_ = 0;
|
||||
bool audio_capture_ = false;
|
||||
int main_window_width_real_ = 720;
|
||||
int main_window_height_real_ = 540;
|
||||
@@ -467,7 +489,7 @@ class Render {
|
||||
bool just_created_ = false;
|
||||
std::string controlled_remote_id_ = "";
|
||||
std::string focused_remote_id_ = "";
|
||||
bool need_to_send_host_info_ = false;
|
||||
std::string remote_client_id_ = "";
|
||||
SDL_Event last_mouse_event;
|
||||
SDL_AudioStream* output_stream_;
|
||||
uint32_t STREAM_REFRESH_EVENT = 0;
|
||||
@@ -504,14 +526,14 @@ class Render {
|
||||
bool need_to_destroy_server_window_ = false;
|
||||
bool server_window_created_ = false;
|
||||
bool server_window_inited_ = false;
|
||||
int server_window_width_default_ = 300;
|
||||
int server_window_height_default_ = 450;
|
||||
float server_window_width_ = 300;
|
||||
float server_window_height_ = 450;
|
||||
float server_window_title_bar_height_ = 50.0f;
|
||||
int server_window_width_default_ = 250;
|
||||
int server_window_height_default_ = 150;
|
||||
float server_window_width_ = 250;
|
||||
float server_window_height_ = 150;
|
||||
float server_window_title_bar_height_ = 30.0f;
|
||||
SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
int server_window_normal_width_ = 300;
|
||||
int server_window_normal_height_ = 450;
|
||||
int server_window_normal_width_ = 250;
|
||||
int server_window_normal_height_ = 150;
|
||||
float server_window_dpi_scaling_w_ = 1.0f;
|
||||
float server_window_dpi_scaling_h_ = 1.0f;
|
||||
|
||||
@@ -573,6 +595,10 @@ class Render {
|
||||
std::unordered_map<uint32_t, std::weak_ptr<SubStreamWindowProperties>>
|
||||
file_id_to_props_;
|
||||
std::shared_mutex file_id_to_props_mutex_;
|
||||
|
||||
// Map file_id to FileTransferState for global file transfer (props == null)
|
||||
std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_;
|
||||
std::shared_mutex file_id_to_transfer_state_mutex_;
|
||||
SDL_AudioDeviceID input_dev_;
|
||||
SDL_AudioDeviceID output_dev_;
|
||||
ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
|
||||
@@ -586,8 +612,8 @@ class Render {
|
||||
uint64_t last_frame_time_;
|
||||
bool show_new_version_icon_ = false;
|
||||
bool show_new_version_icon_in_menu_ = true;
|
||||
uint64_t new_version_icon_last_trigger_time_ = 0;
|
||||
uint64_t new_version_icon_render_start_time_ = 0;
|
||||
double new_version_icon_last_trigger_time_ = 0.0;
|
||||
double new_version_icon_render_start_time_ = 0.0;
|
||||
#ifdef __APPLE__
|
||||
bool show_request_permission_window_ = true;
|
||||
#endif
|
||||
@@ -647,6 +673,10 @@ class Render {
|
||||
|
||||
/* ------ server mode ------ */
|
||||
std::unordered_map<std::string, ConnectionStatus> connection_status_;
|
||||
std::unordered_map<std::string, std::string> connection_host_names_;
|
||||
std::string selected_server_remote_id_ = "";
|
||||
std::string selected_server_remote_hostname_ = "";
|
||||
FileTransferState file_transfer_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "clipboard.h"
|
||||
@@ -237,31 +238,31 @@ void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
|
||||
render->client_properties_.find(remote_id)->second.get();
|
||||
|
||||
if (props->connection_established_) {
|
||||
if (!props->dst_buffer_) {
|
||||
props->dst_buffer_capacity_ = video_frame->size;
|
||||
props->dst_buffer_ = new unsigned char[video_frame->size];
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
|
||||
if (props->dst_buffer_capacity_ < video_frame->size) {
|
||||
delete props->dst_buffer_;
|
||||
props->dst_buffer_capacity_ = video_frame->size;
|
||||
props->dst_buffer_ = new unsigned char[video_frame->size];
|
||||
}
|
||||
if (!props->back_frame_) {
|
||||
props->back_frame_ =
|
||||
std::make_shared<std::vector<unsigned char>>(video_frame->size);
|
||||
}
|
||||
if (props->back_frame_->size() != video_frame->size) {
|
||||
props->back_frame_->resize(video_frame->size);
|
||||
}
|
||||
|
||||
memcpy(props->dst_buffer_, video_frame->data, video_frame->size);
|
||||
bool need_to_update_render_rect = false;
|
||||
if (props->video_width_ != props->video_width_last_ ||
|
||||
props->video_height_ != props->video_height_last_) {
|
||||
need_to_update_render_rect = true;
|
||||
props->video_width_last_ = props->video_width_;
|
||||
props->video_height_last_ = props->video_height_;
|
||||
}
|
||||
props->video_width_ = video_frame->width;
|
||||
props->video_height_ = video_frame->height;
|
||||
props->video_size_ = video_frame->size;
|
||||
std::memcpy(props->back_frame_->data(), video_frame->data,
|
||||
video_frame->size);
|
||||
|
||||
if (need_to_update_render_rect) {
|
||||
render->UpdateRenderRect();
|
||||
const bool size_changed = (props->video_width_ != video_frame->width) ||
|
||||
(props->video_height_ != video_frame->height);
|
||||
if (size_changed) {
|
||||
props->render_rect_dirty_ = true;
|
||||
}
|
||||
|
||||
props->video_width_ = video_frame->width;
|
||||
props->video_height_ = video_frame->height;
|
||||
props->video_size_ = video_frame->size;
|
||||
|
||||
props->front_frame_.swap(props->back_frame_);
|
||||
}
|
||||
|
||||
SDL_Event event;
|
||||
@@ -320,7 +321,20 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
std::string remote_user_id = std::string(user_id, user_id_size);
|
||||
|
||||
static FileReceiver receiver;
|
||||
receiver.SetOnSendAck([render](const FileTransferAck& ack) -> int {
|
||||
receiver.SetOnSendAck([render,
|
||||
remote_user_id](const FileTransferAck& ack) -> int {
|
||||
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
|
||||
if (is_server_sending) {
|
||||
auto props =
|
||||
render->GetSubStreamWindowPropertiesByRemoteId(remote_user_id);
|
||||
if (props) {
|
||||
PeerPtr* peer = props->peer_;
|
||||
return SendReliableDataFrame(
|
||||
peer, reinterpret_cast<const char*>(&ack),
|
||||
sizeof(FileTransferAck), render->file_feedback_label_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
return SendReliableDataFrame(
|
||||
render->peer_, reinterpret_cast<const char*>(&ack),
|
||||
sizeof(FileTransferAck), render->file_feedback_label_.c_str());
|
||||
@@ -361,42 +375,100 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
}
|
||||
}
|
||||
|
||||
Render::FileTransferState* state = nullptr;
|
||||
if (!props) {
|
||||
LOG_WARN("FileTransferAck: no props found for file_id={}", ack.file_id);
|
||||
return;
|
||||
{
|
||||
std::shared_lock lock(render->file_id_to_transfer_state_mutex_);
|
||||
auto it = render->file_id_to_transfer_state_.find(ack.file_id);
|
||||
if (it != render->file_id_to_transfer_state_.end()) {
|
||||
state = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
LOG_WARN("FileTransferAck: no props/state found for file_id={}",
|
||||
ack.file_id);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
state = &props->file_transfer_;
|
||||
}
|
||||
|
||||
// Update progress based on ACK
|
||||
props->file_sent_bytes_ = ack.acked_offset;
|
||||
props->file_total_bytes_ = ack.total_size;
|
||||
state->file_sent_bytes_ = ack.acked_offset;
|
||||
state->file_total_bytes_ = ack.total_size;
|
||||
|
||||
uint32_t rate_bps = 0;
|
||||
{
|
||||
uint32_t data_channel_bitrate =
|
||||
props->net_traffic_stats_.data_outbound_stats.bitrate;
|
||||
if (props) {
|
||||
uint32_t data_channel_bitrate =
|
||||
props->net_traffic_stats_.data_outbound_stats.bitrate;
|
||||
|
||||
if (data_channel_bitrate > 0 && props->file_sending_.load()) {
|
||||
rate_bps = static_cast<uint32_t>(data_channel_bitrate * 0.99f);
|
||||
if (data_channel_bitrate > 0 && state->file_sending_.load()) {
|
||||
rate_bps = static_cast<uint32_t>(data_channel_bitrate * 0.99f);
|
||||
|
||||
uint32_t current_rate = props->file_send_rate_bps_.load();
|
||||
if (current_rate > 0) {
|
||||
// 70% old + 30% new for smoother display
|
||||
rate_bps = static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
|
||||
uint32_t current_rate = state->file_send_rate_bps_.load();
|
||||
if (current_rate > 0) {
|
||||
// 70% old + 30% new for smoother display
|
||||
rate_bps =
|
||||
static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
|
||||
}
|
||||
} else {
|
||||
rate_bps = state->file_send_rate_bps_.load();
|
||||
}
|
||||
} else {
|
||||
rate_bps = props->file_send_rate_bps_.load();
|
||||
// Global transfer: no per-connection bitrate available.
|
||||
// Estimate send rate from ACKed bytes delta over time.
|
||||
const uint32_t current_rate = state->file_send_rate_bps_.load();
|
||||
uint32_t estimated_rate_bps = 0;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
|
||||
uint64_t last_bytes = 0;
|
||||
std::chrono::steady_clock::time_point last_time;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
|
||||
last_bytes = state->file_send_last_bytes_;
|
||||
last_time = state->file_send_last_update_time_;
|
||||
}
|
||||
|
||||
if (state->file_sending_.load() && ack.acked_offset >= last_bytes) {
|
||||
const uint64_t delta_bytes = ack.acked_offset - last_bytes;
|
||||
const double delta_seconds =
|
||||
std::chrono::duration<double>(now - last_time).count();
|
||||
|
||||
if (delta_seconds > 0.0 && delta_bytes > 0) {
|
||||
const double bps =
|
||||
(static_cast<double>(delta_bytes) * 8.0) / delta_seconds;
|
||||
if (bps > 0.0) {
|
||||
const double capped = (std::min)(
|
||||
bps,
|
||||
static_cast<double>((std::numeric_limits<uint32_t>::max)()));
|
||||
estimated_rate_bps = static_cast<uint32_t>(capped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (estimated_rate_bps > 0 && current_rate > 0) {
|
||||
// 70% old + 30% new for smoother display
|
||||
rate_bps = static_cast<uint32_t>(current_rate * 0.7 +
|
||||
estimated_rate_bps * 0.3);
|
||||
} else if (estimated_rate_bps > 0) {
|
||||
rate_bps = estimated_rate_bps;
|
||||
} else {
|
||||
rate_bps = current_rate;
|
||||
}
|
||||
}
|
||||
|
||||
props->file_send_rate_bps_ = rate_bps;
|
||||
props->file_send_last_bytes_ = ack.acked_offset;
|
||||
state->file_send_rate_bps_ = rate_bps;
|
||||
state->file_send_last_bytes_ = ack.acked_offset;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
props->file_send_last_update_time_ = now;
|
||||
state->file_send_last_update_time_ = now;
|
||||
}
|
||||
|
||||
// Update file transfer list: update progress and rate
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
|
||||
for (auto& info : props->file_transfer_list_) {
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto& info : state->file_transfer_list_) {
|
||||
if (info.file_id == ack.file_id) {
|
||||
info.sent_bytes = ack.acked_offset;
|
||||
info.file_size = ack.total_size;
|
||||
@@ -410,8 +482,8 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
if ((ack.flags & 0x01) != 0) {
|
||||
// Transfer completed - receiver has finished receiving the file
|
||||
// Reopen window if it was closed by user
|
||||
props->file_transfer_window_visible_ = true;
|
||||
props->file_sending_ = false; // Mark sending as finished
|
||||
state->file_transfer_window_visible_ = true;
|
||||
state->file_sending_ = false; // Mark sending as finished
|
||||
LOG_INFO(
|
||||
"File transfer completed via ACK, file_id={}, total_size={}, "
|
||||
"acked_offset={}",
|
||||
@@ -419,11 +491,11 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
|
||||
// Update file transfer list: mark as completed
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
|
||||
for (auto& info : props->file_transfer_list_) {
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto& info : state->file_transfer_list_) {
|
||||
if (info.file_id == ack.file_id) {
|
||||
info.status =
|
||||
SubStreamWindowProperties::FileTransferStatus::Completed;
|
||||
Render::FileTransferState::FileTransferStatus::Completed;
|
||||
info.sent_bytes = ack.total_size;
|
||||
break;
|
||||
}
|
||||
@@ -432,9 +504,15 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
|
||||
// Unregister file_id mapping after completion
|
||||
{
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render->file_id_to_props_mutex_);
|
||||
render->file_id_to_props_.erase(ack.file_id);
|
||||
if (props) {
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render->file_id_to_props_mutex_);
|
||||
render->file_id_to_props_.erase(ack.file_id);
|
||||
} else {
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render->file_id_to_transfer_state_mutex_);
|
||||
render->file_id_to_transfer_state_.erase(ack.file_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Process next file in queue
|
||||
@@ -456,24 +534,39 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
// std::shared_lock lock(render->client_properties_mutex_);
|
||||
if (render->client_properties_.find(remote_id) !=
|
||||
render->client_properties_.end()) {
|
||||
// local
|
||||
auto props = render->client_properties_.find(remote_id)->second;
|
||||
if (remote_action.type == ControlType::host_infomation &&
|
||||
props->remote_host_name_.empty()) {
|
||||
props->remote_host_name_ = std::string(remote_action.i.host_name,
|
||||
remote_action.i.host_name_size);
|
||||
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
|
||||
if (remote_action.type == ControlType::host_infomation) {
|
||||
if (render->client_properties_.find(remote_id) !=
|
||||
render->client_properties_.end()) {
|
||||
// client mode
|
||||
auto props = render->client_properties_.find(remote_id)->second;
|
||||
if (props && props->remote_host_name_.empty()) {
|
||||
props->remote_host_name_ = std::string(remote_action.i.host_name,
|
||||
remote_action.i.host_name_size);
|
||||
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
|
||||
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
props->display_info_list_.push_back(
|
||||
DisplayInfo(remote_action.i.display_list[i],
|
||||
remote_action.i.left[i], remote_action.i.top[i],
|
||||
remote_action.i.right[i], remote_action.i.bottom[i]));
|
||||
}
|
||||
}
|
||||
FreeRemoteAction(remote_action);
|
||||
} else {
|
||||
// server mode
|
||||
render->connection_host_names_[remote_id] = std::string(
|
||||
remote_action.i.host_name, remote_action.i.host_name_size);
|
||||
LOG_INFO("Remote hostname: [{}]",
|
||||
render->connection_host_names_[remote_id]);
|
||||
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
props->display_info_list_.push_back(
|
||||
render->display_info_list_.push_back(
|
||||
DisplayInfo(remote_action.i.display_list[i],
|
||||
remote_action.i.left[i], remote_action.i.top[i],
|
||||
remote_action.i.right[i], remote_action.i.bottom[i]));
|
||||
}
|
||||
FreeRemoteAction(remote_action);
|
||||
}
|
||||
FreeRemoteAction(remote_action);
|
||||
} else {
|
||||
// remote
|
||||
if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
|
||||
@@ -520,6 +613,10 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
|
||||
render->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalServerClosed == status) {
|
||||
render->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalFingerprintMismatch == status) {
|
||||
render->signal_connected_ = false;
|
||||
LOG_ERROR("[{}] signal server fingerprint mismatch", client_id);
|
||||
render->config_center_->ClearDefaultCertFingerprint();
|
||||
}
|
||||
} else {
|
||||
if (client_id.rfind("C-", 0) != 0) {
|
||||
@@ -547,6 +644,9 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
|
||||
props->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalServerClosed == status) {
|
||||
props->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalFingerprintMismatch == status) {
|
||||
props->signal_connected_ = false;
|
||||
LOG_ERROR("[{}] signal server fingerprint mismatch", remote_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -568,7 +668,49 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
render->need_to_send_host_info_ = true;
|
||||
{
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num = render->display_info_list_.size();
|
||||
remote_action.i.display_list =
|
||||
(char**)malloc(remote_action.i.display_num * sizeof(char*));
|
||||
remote_action.i.left =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.top =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.right =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.bottom =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
render->display_info_list_[i].name);
|
||||
remote_action.i.display_list[i] =
|
||||
(char*)malloc(render->display_info_list_[i].name.length() + 1);
|
||||
strncpy(remote_action.i.display_list[i],
|
||||
render->display_info_list_[i].name.c_str(),
|
||||
render->display_info_list_[i].name.length());
|
||||
remote_action.i
|
||||
.display_list[i][render->display_info_list_[i].name.length()] =
|
||||
'\0';
|
||||
remote_action.i.left[i] = render->display_info_list_[i].left;
|
||||
remote_action.i.top[i] = render->display_info_list_[i].top;
|
||||
remote_action.i.right[i] = render->display_info_list_[i].right;
|
||||
remote_action.i.bottom[i] = render->display_info_list_[i].bottom;
|
||||
}
|
||||
|
||||
std::string host_name = GetHostName();
|
||||
remote_action.type = ControlType::host_infomation;
|
||||
memcpy(&remote_action.i.host_name, host_name.data(),
|
||||
host_name.size());
|
||||
remote_action.i.host_name[host_name.size()] = '\0';
|
||||
remote_action.i.host_name_size = host_name.size();
|
||||
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
|
||||
render->control_data_label_.c_str());
|
||||
FreeRemoteAction(remote_action);
|
||||
}
|
||||
|
||||
if (!render->need_to_create_stream_window_ &&
|
||||
!render->client_properties_.empty()) {
|
||||
render->need_to_create_stream_window_ = true;
|
||||
@@ -585,12 +727,22 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
case ConnectionStatus::Closed: {
|
||||
props->connection_established_ = false;
|
||||
props->mouse_control_button_pressed_ = false;
|
||||
if (props->dst_buffer_ && props->stream_texture_) {
|
||||
memset(props->dst_buffer_, 0, props->dst_buffer_capacity_);
|
||||
SDL_UpdateTexture(props->stream_texture_, NULL, props->dst_buffer_,
|
||||
props->texture_width_);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
props->video_width_ = 0;
|
||||
props->video_height_ = 0;
|
||||
props->video_size_ = 0;
|
||||
props->render_rect_dirty_ = true;
|
||||
props->stream_cleanup_pending_ = true;
|
||||
}
|
||||
render->CleanSubStreamWindowProperties(props);
|
||||
|
||||
SDL_Event event;
|
||||
event.type = render->STREAM_REFRESH_EVENT;
|
||||
event.user.data1 = props.get();
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -623,11 +775,54 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
{
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num = render->display_info_list_.size();
|
||||
remote_action.i.display_list =
|
||||
(char**)malloc(remote_action.i.display_num * sizeof(char*));
|
||||
remote_action.i.left =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.top =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.right =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.bottom =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
render->display_info_list_[i].name);
|
||||
remote_action.i.display_list[i] =
|
||||
(char*)malloc(render->display_info_list_[i].name.length() + 1);
|
||||
strncpy(remote_action.i.display_list[i],
|
||||
render->display_info_list_[i].name.c_str(),
|
||||
render->display_info_list_[i].name.length());
|
||||
remote_action.i
|
||||
.display_list[i][render->display_info_list_[i].name.length()] =
|
||||
'\0';
|
||||
remote_action.i.left[i] = render->display_info_list_[i].left;
|
||||
remote_action.i.top[i] = render->display_info_list_[i].top;
|
||||
remote_action.i.right[i] = render->display_info_list_[i].right;
|
||||
remote_action.i.bottom[i] = render->display_info_list_[i].bottom;
|
||||
}
|
||||
|
||||
std::string host_name = GetHostName();
|
||||
remote_action.type = ControlType::host_infomation;
|
||||
memcpy(&remote_action.i.host_name, host_name.data(),
|
||||
host_name.size());
|
||||
remote_action.i.host_name[host_name.size()] = '\0';
|
||||
remote_action.i.host_name_size = host_name.size();
|
||||
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(render->peer_, msg.data(), msg.size(),
|
||||
render->control_data_label_.c_str());
|
||||
FreeRemoteAction(remote_action);
|
||||
}
|
||||
|
||||
render->need_to_create_server_window_ = true;
|
||||
render->need_to_send_host_info_ = true;
|
||||
render->is_server_mode_ = true;
|
||||
render->start_screen_capturer_ = true;
|
||||
render->start_speaker_capturer_ = true;
|
||||
render->remote_client_id_ = remote_id;
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
render->start_mouse_controller_ = false;
|
||||
render->start_keyboard_capturer_ = false;
|
||||
@@ -656,7 +851,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
render->start_speaker_capturer_ = false;
|
||||
render->start_mouse_controller_ = false;
|
||||
render->start_keyboard_capturer_ = false;
|
||||
render->need_to_send_host_info_ = false;
|
||||
render->remote_client_id_ = "";
|
||||
if (props) props->connection_established_ = false;
|
||||
if (render->audio_capture_) {
|
||||
render->StopSpeakerCapturer();
|
||||
@@ -681,11 +876,11 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
}
|
||||
}
|
||||
|
||||
void Render::NetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data) {
|
||||
void Render::OnNetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data) {
|
||||
Render* render = (Render*)user_data;
|
||||
if (!render) {
|
||||
return;
|
||||
|
||||
@@ -15,18 +15,6 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
std::string OpenFileDialog(std::string title) {
|
||||
const char* path = tinyfd_openFileDialog(title.c_str(),
|
||||
"", // default path
|
||||
0, // number of filters
|
||||
nullptr, // filters
|
||||
nullptr, // filter description
|
||||
0 // no multiple selection
|
||||
);
|
||||
|
||||
return path ? path : "";
|
||||
}
|
||||
|
||||
int CountDigits(int number) {
|
||||
if (number == 0) return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
@@ -53,13 +41,29 @@ int LossRateDisplay(float loss_rate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string Render::OpenFileDialog(std::string title) {
|
||||
const char* path = tinyfd_openFileDialog(title.c_str(),
|
||||
"", // default path
|
||||
0, // number of filters
|
||||
nullptr, // filters
|
||||
nullptr, // filter description
|
||||
0 // no multiple selection
|
||||
);
|
||||
|
||||
return path ? path : "";
|
||||
}
|
||||
|
||||
void Render::ProcessSelectedFile(
|
||||
const std::string& path, std::shared_ptr<SubStreamWindowProperties>& props,
|
||||
const std::string& file_label) {
|
||||
const std::string& path,
|
||||
const std::shared_ptr<SubStreamWindowProperties>& props,
|
||||
const std::string& file_label, const std::string& remote_id) {
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferState* file_transfer_state =
|
||||
props ? &props->file_transfer_ : &file_transfer_;
|
||||
|
||||
LOG_INFO("Selected file: {}", path.c_str());
|
||||
|
||||
std::filesystem::path file_path = std::filesystem::u8path(path);
|
||||
@@ -74,47 +78,51 @@ void Render::ProcessSelectedFile(
|
||||
|
||||
// Add file to transfer list
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
|
||||
SubStreamWindowProperties::FileTransferInfo info;
|
||||
std::lock_guard<std::mutex> lock(
|
||||
file_transfer_state->file_transfer_list_mutex_);
|
||||
FileTransferState::FileTransferInfo info;
|
||||
info.file_name = file_path.filename().u8string();
|
||||
info.file_path = file_path; // Store full path for precise matching
|
||||
info.file_size = file_size;
|
||||
info.status = SubStreamWindowProperties::FileTransferStatus::Queued;
|
||||
info.status = FileTransferState::FileTransferStatus::Queued;
|
||||
info.sent_bytes = 0;
|
||||
info.file_id = 0;
|
||||
info.rate_bps = 0;
|
||||
props->file_transfer_list_.push_back(info);
|
||||
file_transfer_state->file_transfer_list_.push_back(info);
|
||||
}
|
||||
props->file_transfer_window_visible_ = true;
|
||||
file_transfer_state->file_transfer_window_visible_ = true;
|
||||
|
||||
if (props->file_sending_.load()) {
|
||||
if (file_transfer_state->file_sending_.load()) {
|
||||
// Add to queue
|
||||
size_t queue_size = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_queue_mutex_);
|
||||
SubStreamWindowProperties::QueuedFile queued_file;
|
||||
std::lock_guard<std::mutex> lock(file_transfer_state->file_queue_mutex_);
|
||||
FileTransferState::QueuedFile queued_file;
|
||||
queued_file.file_path = file_path;
|
||||
queued_file.file_label = file_label;
|
||||
props->file_send_queue_.push(queued_file);
|
||||
queue_size = props->file_send_queue_.size();
|
||||
queued_file.remote_id = remote_id;
|
||||
file_transfer_state->file_send_queue_.push(queued_file);
|
||||
queue_size = file_transfer_state->file_send_queue_.size();
|
||||
}
|
||||
LOG_INFO("File added to queue: {} ({} files in queue)",
|
||||
file_path.filename().string().c_str(), queue_size);
|
||||
} else {
|
||||
StartFileTransfer(props, file_path, file_label);
|
||||
StartFileTransfer(props, file_path, file_label, remote_id);
|
||||
|
||||
if (props->file_sending_.load()) {
|
||||
if (file_transfer_state->file_sending_.load()) {
|
||||
} else {
|
||||
// Failed to start (race condition: another file started between
|
||||
// check and call) Add to queue
|
||||
size_t queue_size = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_queue_mutex_);
|
||||
SubStreamWindowProperties::QueuedFile queued_file;
|
||||
std::lock_guard<std::mutex> lock(
|
||||
file_transfer_state->file_queue_mutex_);
|
||||
FileTransferState::QueuedFile queued_file;
|
||||
queued_file.file_path = file_path;
|
||||
queued_file.file_label = file_label;
|
||||
props->file_send_queue_.push(queued_file);
|
||||
queue_size = props->file_send_queue_.size();
|
||||
queued_file.remote_id = remote_id;
|
||||
file_transfer_state->file_send_queue_.push(queued_file);
|
||||
queue_size = file_transfer_state->file_send_queue_.size();
|
||||
}
|
||||
LOG_INFO(
|
||||
"File added to queue after race condition: {} ({} files in "
|
||||
@@ -275,7 +283,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
std::string title =
|
||||
localization::select_file[localization_language_index_];
|
||||
std::string path = OpenFileDialog(title);
|
||||
this->ProcessSelectedFile(path, props, file_label_);
|
||||
ProcessSelectedFile(path, props, file_label_);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
#define NEW_VERSION_ICON_RENDER_TIME_INTERVAL 2000
|
||||
constexpr double kNewVersionIconBlinkIntervalSec = 2.0;
|
||||
constexpr double kNewVersionIconBlinkOnTimeSec = 1.0;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -15,14 +16,28 @@ int Render::TitleBar(bool main_window) {
|
||||
float title_bar_button_width = title_bar_button_width_;
|
||||
float title_bar_button_height = title_bar_button_height_;
|
||||
if (main_window) {
|
||||
title_bar_width = io.DisplaySize.x;
|
||||
title_bar_height = io.DisplaySize.y * TITLE_BAR_HEIGHT;
|
||||
title_bar_height_padding = io.DisplaySize.y * (TITLE_BAR_HEIGHT + 0.01f);
|
||||
title_bar_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH;
|
||||
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT;
|
||||
title_bar_height_ = title_bar_height;
|
||||
title_bar_button_width_ = title_bar_button_width;
|
||||
title_bar_button_height_ = title_bar_button_height;
|
||||
// When the main window is minimized, Dear ImGui may report DisplaySize as
|
||||
// (0, 0). Do not overwrite shared title-bar metrics with zeros, otherwise
|
||||
// stream/server windows (which reuse these metrics) will lose their title
|
||||
// bars and appear collapsed.
|
||||
if (io.DisplaySize.x > 0.0f && io.DisplaySize.y > 0.0f) {
|
||||
title_bar_width = io.DisplaySize.x;
|
||||
title_bar_height = io.DisplaySize.y * TITLE_BAR_HEIGHT;
|
||||
title_bar_height_padding = io.DisplaySize.y * (TITLE_BAR_HEIGHT + 0.01f);
|
||||
title_bar_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH;
|
||||
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT;
|
||||
|
||||
title_bar_height_ = title_bar_height;
|
||||
title_bar_button_width_ = title_bar_button_width;
|
||||
title_bar_button_height_ = title_bar_button_height;
|
||||
} else {
|
||||
// Keep using last known good values.
|
||||
title_bar_width = title_bar_width_;
|
||||
title_bar_height = title_bar_height_;
|
||||
title_bar_height_padding = title_bar_height_;
|
||||
title_bar_button_width = title_bar_button_width_;
|
||||
title_bar_button_height = title_bar_button_height_;
|
||||
}
|
||||
} else {
|
||||
title_bar_width = io.DisplaySize.x;
|
||||
title_bar_height = title_bar_button_height_;
|
||||
@@ -92,13 +107,11 @@ int Render::TitleBar(bool main_window) {
|
||||
|
||||
std::string about_str = localization::about[localization_language_index_];
|
||||
if (update_available_) {
|
||||
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
const double now_time = ImGui::GetTime();
|
||||
|
||||
// every 2 seconds
|
||||
if (now_time - new_version_icon_last_trigger_time_ >=
|
||||
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) {
|
||||
kNewVersionIconBlinkIntervalSec) {
|
||||
show_new_version_icon_ = true;
|
||||
new_version_icon_render_start_time_ = now_time;
|
||||
new_version_icon_last_trigger_time_ = now_time;
|
||||
@@ -106,9 +119,9 @@ int Render::TitleBar(bool main_window) {
|
||||
|
||||
// render for 1 second
|
||||
if (show_new_version_icon_) {
|
||||
about_str = about_str + " " + ICON_FA_TRIANGLE_EXCLAMATION;
|
||||
about_str = about_str + " " + ICON_FA_CIRCLE_ARROW_UP;
|
||||
if (now_time - new_version_icon_render_start_time_ >=
|
||||
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) {
|
||||
kNewVersionIconBlinkOnTimeSec) {
|
||||
show_new_version_icon_ = false;
|
||||
}
|
||||
} else {
|
||||
@@ -137,13 +150,11 @@ int Render::TitleBar(bool main_window) {
|
||||
}
|
||||
|
||||
if (update_available_ && show_new_version_icon_in_menu_) {
|
||||
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
const double now_time = ImGui::GetTime();
|
||||
|
||||
// every 2 seconds
|
||||
if (now_time - new_version_icon_last_trigger_time_ >=
|
||||
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) {
|
||||
kNewVersionIconBlinkIntervalSec) {
|
||||
show_new_version_icon_ = true;
|
||||
new_version_icon_render_start_time_ = now_time;
|
||||
new_version_icon_last_trigger_time_ = now_time;
|
||||
@@ -152,14 +163,13 @@ int Render::TitleBar(bool main_window) {
|
||||
// render for 1 second
|
||||
if (show_new_version_icon_) {
|
||||
ImGui::SetWindowFontScale(0.6f);
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(bar_pos_x + title_bar_button_width * 0.15f,
|
||||
bar_pos_y - title_bar_button_width * 0.325f));
|
||||
ImGui::Text(ICON_FA_TRIANGLE_EXCLAMATION);
|
||||
ImGui::SetCursorPos(ImVec2(bar_pos_x + title_bar_button_width * 0.21f,
|
||||
bar_pos_y - title_bar_button_width * 0.24f));
|
||||
ImGui::Text(ICON_FA_CIRCLE_ARROW_UP);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
if (now_time - new_version_icon_render_start_time_ >=
|
||||
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) {
|
||||
kNewVersionIconBlinkOnTimeSec) {
|
||||
show_new_version_icon_ = false;
|
||||
}
|
||||
}
|
||||
@@ -187,6 +197,11 @@ int Render::TitleBar(bool main_window) {
|
||||
std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS;
|
||||
if (ImGui::Button(window_minimize_button.c_str(),
|
||||
ImVec2(title_bar_button_width, title_bar_button_height))) {
|
||||
if (main_window) {
|
||||
last_main_minimize_request_tick_ = SDL_GetTicks();
|
||||
} else {
|
||||
last_stream_minimize_request_tick_ = SDL_GetTicks();
|
||||
}
|
||||
SDL_MinimizeWindow(main_window ? main_window_ : stream_window_);
|
||||
}
|
||||
draw_list->AddLine(
|
||||
|
||||
@@ -30,12 +30,14 @@ int BitrateDisplay(int bitrate) {
|
||||
|
||||
int Render::FileTransferWindow(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_;
|
||||
|
||||
// Only show window if there are files in transfer list or currently
|
||||
// transferring
|
||||
std::vector<SubStreamWindowProperties::FileTransferInfo> file_list;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
|
||||
file_list = props->file_transfer_list_;
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
file_list = state->file_transfer_list_;
|
||||
}
|
||||
|
||||
// Sort file list: Sending first, then Completed, then Queued, then Failed
|
||||
@@ -66,7 +68,7 @@ int Render::FileTransferWindow(
|
||||
// It will be reopened automatically when:
|
||||
// 1. A file transfer completes (in render_callback.cpp)
|
||||
// 2. A new file starts sending from queue (in render.cpp)
|
||||
if (!props->file_transfer_window_visible_) {
|
||||
if (!state->file_transfer_window_visible_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -115,7 +117,7 @@ int Render::FileTransferWindow(
|
||||
|
||||
// Close button handling
|
||||
if (!window_opened) {
|
||||
props->file_transfer_window_visible_ = false;
|
||||
state->file_transfer_window_visible_ = false;
|
||||
ImGui::End();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,48 +1,46 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
constexpr float kDragThresholdPx = 3.0f;
|
||||
int CountDigits(int number) {
|
||||
if (number == 0) return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
}
|
||||
|
||||
// Handles dragging for the *last submitted ImGui item*.
|
||||
// `reset_on_deactivate` should be false when the caller needs to know whether a
|
||||
// deactivation was a click (no drag) vs a drag-release (dragging==true).
|
||||
inline void HandleWindowDragForLastItem(SDL_Window* window, bool& dragging,
|
||||
float& start_mouse_x,
|
||||
float& start_mouse_y, int& start_win_x,
|
||||
int& start_win_y,
|
||||
bool reset_on_deactivate = true) {
|
||||
if (!window) {
|
||||
return;
|
||||
void BitrateDisplay(uint32_t bitrate) {
|
||||
const int num_of_digits = CountDigits(static_cast<int>(bitrate));
|
||||
if (num_of_digits <= 3) {
|
||||
ImGui::Text("%u bps", bitrate);
|
||||
} else if (num_of_digits > 3 && num_of_digits <= 6) {
|
||||
ImGui::Text("%u kbps", bitrate / 1000);
|
||||
} else {
|
||||
ImGui::Text("%.1f mbps", bitrate / 1000000.0f);
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::IsItemActivated()) {
|
||||
SDL_GetGlobalMouseState(&start_mouse_x, &start_mouse_y);
|
||||
SDL_GetWindowPosition(window, &start_win_x, &start_win_y);
|
||||
dragging = false;
|
||||
}
|
||||
|
||||
if (ImGui::IsItemActive()) {
|
||||
if (!dragging &&
|
||||
ImGui::IsMouseDragging(ImGuiMouseButton_Left, kDragThresholdPx)) {
|
||||
dragging = true;
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
float mouse_x = 0.0f;
|
||||
float mouse_y = 0.0f;
|
||||
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
|
||||
const int dx = (int)(mouse_x - start_mouse_x);
|
||||
const int dy = (int)(mouse_y - start_mouse_y);
|
||||
SDL_SetWindowPosition(window, start_win_x + dx, start_win_y + dy);
|
||||
}
|
||||
}
|
||||
|
||||
if (reset_on_deactivate && ImGui::IsItemDeactivated()) {
|
||||
dragging = false;
|
||||
std::string FormatBytes(uint64_t bytes) {
|
||||
char buf[64];
|
||||
if (bytes < 1024ULL) {
|
||||
std::snprintf(buf, sizeof(buf), "%llu B", (unsigned long long)bytes);
|
||||
} else if (bytes < 1024ULL * 1024ULL) {
|
||||
std::snprintf(buf, sizeof(buf), "%.2f KB", bytes / 1024.0);
|
||||
} else if (bytes < 1024ULL * 1024ULL * 1024ULL) {
|
||||
std::snprintf(buf, sizeof(buf), "%.2f MB", bytes / (1024.0 * 1024.0));
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "%.2f GB",
|
||||
bytes / (1024.0 * 1024.0 * 1024.0));
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -57,43 +55,7 @@ int Render::ServerWindow() {
|
||||
ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoScrollWithMouse);
|
||||
|
||||
// Collapsed mode: no buttons; drag to move; click to restore.
|
||||
if (server_window_collapsed_) {
|
||||
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
|
||||
ImGui::InvisibleButton("##server_collapsed_area",
|
||||
ImVec2(server_window_width_, server_window_height_));
|
||||
|
||||
HandleWindowDragForLastItem(server_window_,
|
||||
server_window_collapsed_dragging_,
|
||||
server_window_collapsed_drag_start_mouse_x_,
|
||||
server_window_collapsed_drag_start_mouse_y_,
|
||||
server_window_collapsed_drag_start_win_x_,
|
||||
server_window_collapsed_drag_start_win_y_,
|
||||
/*reset_on_deactivate=*/false);
|
||||
|
||||
const bool request_restore =
|
||||
ImGui::IsItemDeactivated() && !server_window_collapsed_dragging_;
|
||||
if (ImGui::IsItemDeactivated()) {
|
||||
server_window_collapsed_dragging_ = false;
|
||||
}
|
||||
|
||||
if (request_restore && server_window_) {
|
||||
int w = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int h = 0;
|
||||
SDL_GetWindowSize(server_window_, &w, &h);
|
||||
SDL_GetWindowPosition(server_window_, &x, &y);
|
||||
|
||||
const int normal_h = server_window_normal_height_;
|
||||
SDL_SetWindowSize(server_window_, w, normal_h);
|
||||
SDL_SetWindowPosition(server_window_, x, y);
|
||||
server_window_collapsed_ = false;
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
return 0;
|
||||
}
|
||||
server_window_title_bar_height_ = title_bar_height_;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
@@ -105,97 +67,302 @@ int Render::ServerWindow() {
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
|
||||
float server_title_bar_button_width = server_window_title_bar_height_;
|
||||
float server_title_bar_button_height = server_window_title_bar_height_;
|
||||
|
||||
// Drag area: the title bar excluding the right-side buttons.
|
||||
// Collapse/expand toggle button (FontAwesome icon).
|
||||
{
|
||||
const float drag_w =
|
||||
server_window_width_ - server_title_bar_button_width * 2;
|
||||
const float drag_h = server_title_bar_button_height;
|
||||
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
|
||||
ImGui::InvisibleButton("##server_title_drag_area", ImVec2(drag_w, drag_h));
|
||||
|
||||
HandleWindowDragForLastItem(
|
||||
server_window_, server_window_dragging_,
|
||||
server_window_drag_start_mouse_x_, server_window_drag_start_mouse_y_,
|
||||
server_window_drag_start_win_x_, server_window_drag_start_win_y_);
|
||||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
||||
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
|
||||
float minimize_button_pos_x =
|
||||
server_window_width_ - server_title_bar_button_width * 2;
|
||||
ImGui::SetCursorPos(ImVec2(minimize_button_pos_x, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
const char* icon =
|
||||
server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP;
|
||||
std::string toggle_label = std::string(icon) + "##server_toggle";
|
||||
if (ImGui::Button(toggle_label.c_str(),
|
||||
ImVec2(server_title_bar_button_width,
|
||||
server_title_bar_button_height))) {
|
||||
if (server_window_) {
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
SDL_GetWindowSize(server_window_, &w, &h);
|
||||
SDL_GetWindowPosition(server_window_, &x, &y);
|
||||
|
||||
float minimize_pos_x =
|
||||
minimize_button_pos_x + server_title_bar_button_width * 0.33f;
|
||||
float minimize_pos_y = server_title_bar_button_height * 0.5f;
|
||||
std::string server_minimize_button = "##minimize"; // ICON_FA_MINUS;
|
||||
if (ImGui::Button(server_minimize_button.c_str(),
|
||||
ImVec2(server_title_bar_button_width,
|
||||
server_title_bar_button_height))) {
|
||||
if (server_window_) {
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
SDL_GetWindowSize(server_window_, &w, &h);
|
||||
SDL_GetWindowPosition(server_window_, &x, &y);
|
||||
|
||||
const int collapsed_h = (int)server_window_title_bar_height_;
|
||||
// Collapse upward: keep top edge stable.
|
||||
SDL_SetWindowSize(server_window_, w, collapsed_h);
|
||||
SDL_SetWindowPosition(server_window_, x, y);
|
||||
server_window_collapsed_ = true;
|
||||
if (server_window_collapsed_) {
|
||||
const int normal_h = server_window_normal_height_;
|
||||
SDL_SetWindowSize(server_window_, w, normal_h);
|
||||
SDL_SetWindowPosition(server_window_, x, y);
|
||||
server_window_collapsed_ = false;
|
||||
} else {
|
||||
const int collapsed_h = (int)server_window_title_bar_height_;
|
||||
// Collapse upward: keep top edge stable.
|
||||
SDL_SetWindowSize(server_window_, w, collapsed_h);
|
||||
SDL_SetWindowPosition(server_window_, x, y);
|
||||
server_window_collapsed_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
draw_list->AddLine(
|
||||
ImVec2(minimize_pos_x, minimize_pos_y),
|
||||
ImVec2(minimize_pos_x + server_title_bar_button_width * 0.33f,
|
||||
minimize_pos_y),
|
||||
IM_COL32(0, 0, 0, 255));
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
float xmark_button_pos_x =
|
||||
server_window_width_ - server_title_bar_button_width;
|
||||
ImGui::SetCursorPos(ImVec2(xmark_button_pos_x, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0, 0, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0, 0, 0.5f));
|
||||
|
||||
float xmark_pos_x = xmark_button_pos_x + server_title_bar_button_width * 0.5f;
|
||||
float xmark_pos_y = server_title_bar_button_height * 0.5f;
|
||||
float xmark_size = server_title_bar_button_width * 0.33f;
|
||||
std::string server_close_button = "##xmark"; // ICON_FA_XMARK;
|
||||
if (ImGui::Button(server_close_button.c_str(),
|
||||
ImVec2(server_title_bar_button_width,
|
||||
server_title_bar_button_height))) {
|
||||
LOG_ERROR("Close button clicked");
|
||||
LeaveConnection(peer_, self_hosted_id_);
|
||||
}
|
||||
|
||||
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
|
||||
xmark_pos_y - xmark_size / 2 + 0.75f),
|
||||
ImVec2(xmark_pos_x + xmark_size / 2 - 1.5f,
|
||||
xmark_pos_y + xmark_size / 2 - 0.5f),
|
||||
IM_COL32(0, 0, 0, 255));
|
||||
draw_list->AddLine(
|
||||
ImVec2(xmark_pos_x + xmark_size / 2 - 1.75f,
|
||||
xmark_pos_y - xmark_size / 2 + 0.75f),
|
||||
ImVec2(xmark_pos_x - xmark_size / 2, xmark_pos_y + xmark_size / 2 - 1.0f),
|
||||
IM_COL32(0, 0, 0, 255));
|
||||
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
RemoteClientInfoWindow();
|
||||
|
||||
ImGui::End();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::RemoteClientInfoWindow() {
|
||||
float remote_client_info_window_width = server_window_width_ * 0.8f;
|
||||
float remote_client_info_window_height =
|
||||
(server_window_height_ - server_window_title_bar_height_) * 0.9f;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);
|
||||
ImGui::BeginChild(
|
||||
"RemoteClientInfoWindow",
|
||||
ImVec2(remote_client_info_window_width, remote_client_info_window_height),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
|
||||
|
||||
std::vector<std::string> remote_hostnames;
|
||||
remote_hostnames.reserve(connection_host_names_.size());
|
||||
for (const auto& kv : connection_host_names_) {
|
||||
remote_hostnames.push_back(kv.second);
|
||||
}
|
||||
|
||||
auto find_remote_id_by_hostname =
|
||||
[this](const std::string& hostname) -> std::string {
|
||||
for (const auto& kv : connection_host_names_) {
|
||||
if (kv.second == hostname) {
|
||||
return kv.first;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
if (!selected_server_remote_hostname_.empty()) {
|
||||
if (std::find(remote_hostnames.begin(), remote_hostnames.end(),
|
||||
selected_server_remote_hostname_) == remote_hostnames.end()) {
|
||||
selected_server_remote_hostname_.clear();
|
||||
selected_server_remote_id_.clear();
|
||||
}
|
||||
}
|
||||
if (selected_server_remote_hostname_.empty() && !remote_hostnames.empty()) {
|
||||
selected_server_remote_hostname_ = remote_hostnames.front();
|
||||
selected_server_remote_id_ =
|
||||
find_remote_id_by_hostname(selected_server_remote_hostname_);
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(font_scale);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%s",
|
||||
localization::controller[localization_language_index_].c_str());
|
||||
ImGui::SameLine();
|
||||
|
||||
const char* selected_preview = "-";
|
||||
if (!selected_server_remote_hostname_.empty()) {
|
||||
selected_preview = selected_server_remote_hostname_.c_str();
|
||||
} else if (!remote_client_id_.empty()) {
|
||||
selected_preview = remote_client_id_.c_str();
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(remote_client_info_window_width *
|
||||
(localization_language_index_ == 0 ? 0.68f : 0.62f));
|
||||
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
if (ImGui::BeginCombo("##server_remote_id", selected_preview)) {
|
||||
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
|
||||
for (int i = 0; i < static_cast<int>(remote_hostnames.size()); i++) {
|
||||
const bool selected =
|
||||
(remote_hostnames[i] == selected_server_remote_hostname_);
|
||||
if (ImGui::Selectable(remote_hostnames[i].c_str(), selected)) {
|
||||
selected_server_remote_hostname_ = remote_hostnames[i];
|
||||
selected_server_remote_id_ =
|
||||
find_remote_id_by_hostname(selected_server_remote_hostname_);
|
||||
}
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::SetWindowFontScale(font_scale);
|
||||
|
||||
if (!selected_server_remote_id_.empty()) {
|
||||
auto it = connection_status_.find(selected_server_remote_id_);
|
||||
const ConnectionStatus status = (it == connection_status_.end())
|
||||
? ConnectionStatus::Closed
|
||||
: it->second;
|
||||
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::connection_status[localization_language_index_].c_str());
|
||||
ImGui::SameLine();
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_connected[localization_language_index_].c_str());
|
||||
break;
|
||||
case ConnectionStatus::Connecting:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_connecting[localization_language_index_].c_str());
|
||||
break;
|
||||
case ConnectionStatus::Disconnected:
|
||||
ImGui::Text("%s",
|
||||
localization::p2p_disconnected[localization_language_index_]
|
||||
.c_str());
|
||||
break;
|
||||
case ConnectionStatus::Failed:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_failed[localization_language_index_].c_str());
|
||||
break;
|
||||
case ConnectionStatus::Closed:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_closed[localization_language_index_].c_str());
|
||||
break;
|
||||
default:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_failed[localization_language_index_].c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text(
|
||||
"%s", localization::file_transfer[localization_language_index_].c_str());
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(
|
||||
localization::select_file[localization_language_index_].c_str())) {
|
||||
std::string title = localization::select_file[localization_language_index_];
|
||||
std::string path = OpenFileDialog(title);
|
||||
LOG_INFO("Selected file path: {}", path.c_str());
|
||||
|
||||
ProcessSelectedFile(path, nullptr, file_label_, selected_server_remote_id_);
|
||||
}
|
||||
|
||||
if (file_transfer_.file_transfer_window_visible_) {
|
||||
ImGui::SameLine();
|
||||
const bool is_sending = file_transfer_.file_sending_.load();
|
||||
|
||||
if (is_sending) {
|
||||
// Simple animation: cycle icon every 0.5s while sending.
|
||||
static const char* kFileTransferIcons[] = {ICON_FA_ANGLE_UP,
|
||||
ICON_FA_ANGLES_UP};
|
||||
const int icon_index = static_cast<int>(ImGui::GetTime() / 0.5) %
|
||||
(static_cast<int>(sizeof(kFileTransferIcons) /
|
||||
sizeof(kFileTransferIcons[0])));
|
||||
ImGui::Text("%s", kFileTransferIcons[icon_index]);
|
||||
} else {
|
||||
// Completed.
|
||||
ImGui::Text("%s", ICON_FA_CHECK);
|
||||
}
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
const uint64_t sent_bytes = file_transfer_.file_sent_bytes_.load();
|
||||
const uint64_t total_bytes = file_transfer_.file_total_bytes_.load();
|
||||
const uint32_t rate_bps = file_transfer_.file_send_rate_bps_.load();
|
||||
|
||||
float progress = 0.0f;
|
||||
if (total_bytes > 0) {
|
||||
progress =
|
||||
static_cast<float>(sent_bytes) / static_cast<float>(total_bytes);
|
||||
progress = (std::max)(0.0f, (std::min)(1.0f, progress));
|
||||
}
|
||||
|
||||
std::string current_file_name;
|
||||
const uint32_t current_file_id = file_transfer_.current_file_id_.load();
|
||||
if (current_file_id != 0) {
|
||||
std::lock_guard<std::mutex> lock(
|
||||
file_transfer_.file_transfer_list_mutex_);
|
||||
for (const auto& info : file_transfer_.file_transfer_list_) {
|
||||
if (info.file_id == current_file_id) {
|
||||
current_file_name = info.file_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
if (server_windows_system_chinese_font_) {
|
||||
ImGui::PushFont(server_windows_system_chinese_font_);
|
||||
}
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
if (!current_file_name.empty()) {
|
||||
ImGui::Text("%s", current_file_name.c_str());
|
||||
}
|
||||
if (total_bytes > 0) {
|
||||
const std::string sent_str = FormatBytes(sent_bytes);
|
||||
const std::string total_str = FormatBytes(total_bytes);
|
||||
ImGui::Text("%s / %s", sent_str.c_str(), total_str.c_str());
|
||||
}
|
||||
|
||||
const float text_height = ImGui::GetTextLineHeight();
|
||||
char overlay[32];
|
||||
std::snprintf(overlay, sizeof(overlay), "%.1f%%", progress * 100.0f);
|
||||
ImGui::ProgressBar(progress, ImVec2(180.0f, text_height), overlay);
|
||||
BitrateDisplay(rate_bps);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
if (server_windows_system_chinese_font_) {
|
||||
ImGui::PopFont();
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
float close_connection_button_width = server_window_width_ * 0.1f;
|
||||
float close_connection_button_height = remote_client_info_window_height;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0.5f, 0.5f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 5.0f);
|
||||
ImGui::SetWindowFontScale(font_scale);
|
||||
if (ImGui::Button(ICON_FA_XMARK, ImVec2(close_connection_button_width,
|
||||
close_connection_button_height))) {
|
||||
if (peer_ && !selected_server_remote_id_.empty()) {
|
||||
LeaveConnection(peer_, selected_server_remote_id_.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
Submodule submodules/minirtc updated: a282340f8b...27f721015b
Reference in New Issue
Block a user