mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
[feat] add connection latency to network statistics
This commit is contained in:
@@ -2803,7 +2803,7 @@ void GuiApplication::SyncStreamWindow() {
|
||||
ui_->display_model->set_vector(std::move(displays));
|
||||
(*ui_->stream)->set_selected_display(props->selected_display_);
|
||||
|
||||
const auto& net = props->net_traffic_stats_;
|
||||
const auto net = props->net_traffic_stats_.Load();
|
||||
(*ui_->stream)->set_stats_encryption_enabled(
|
||||
status == ConnectionStatus::Connected && net.srtp_active);
|
||||
std::vector<ui::NetworkStatsRow> stats_rows;
|
||||
@@ -2925,6 +2925,17 @@ void GuiApplication::SyncStreamWindow() {
|
||||
static_cast<int>(std::lround(latency->average_ms))) +
|
||||
" ms"
|
||||
: "—"));
|
||||
const bool rtt_available = props->net_traffic_stats_button_pressed_ &&
|
||||
status == ConnectionStatus::Connected &&
|
||||
net.rtt_ms >= 0;
|
||||
std::string connection_latency = "—";
|
||||
if (rtt_available) {
|
||||
connection_latency =
|
||||
net.rtt_ms < 1
|
||||
? "<1 ms"
|
||||
: std::to_string(static_cast<int>(std::lround(net.rtt_ms))) + " ms";
|
||||
}
|
||||
(*ui_->stream)->set_stats_connection_latency(UiText(connection_latency));
|
||||
}
|
||||
|
||||
void GuiApplication::SyncStreamVideoFrame() {
|
||||
|
||||
@@ -306,6 +306,7 @@ struct TranslationRow {
|
||||
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
|
||||
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
|
||||
X(video_latency, u8"视频延时", "Video delay", u8"Задержка") \
|
||||
X(connection_latency, u8"连接延时", "Connection RTT", u8"Задержка соединения") \
|
||||
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
|
||||
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
|
||||
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
|
||||
|
||||
@@ -298,7 +298,7 @@ void FileTransferManager::HandleAck(const char *data, size_t size) {
|
||||
uint32_t rate_bps = 0;
|
||||
if (props) {
|
||||
const uint32_t bitrate =
|
||||
props->net_traffic_stats_.data_outbound_stats.bitrate;
|
||||
props->net_traffic_stats_.DataOutboundBitrate();
|
||||
if (bitrate > 0 && state->file_sending_.load()) {
|
||||
rate_bps = static_cast<uint32_t>(bitrate * 0.99f);
|
||||
const uint32_t current_rate = state->file_send_rate_bps_.load();
|
||||
|
||||
@@ -417,6 +417,7 @@ void GuiRuntime::WaitForSessionCleanup() {
|
||||
|
||||
void GuiRuntime::ResetRemoteSessionResources(
|
||||
std::shared_ptr<RemoteSession> props) {
|
||||
props->net_traffic_stats_.Reset();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->video_latency_->Reset();
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-26
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _NET_TRAFFIC_STATS_CACHE_H_
|
||||
#define _NET_TRAFFIC_STATS_CACHE_H_
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
// Fields are independent observations and may come from different reports.
|
||||
// Relaxed accesses keep each field safe without publishing any other state.
|
||||
class NetTrafficStatsCache {
|
||||
static_assert(std::atomic<uint32_t>::is_always_lock_free &&
|
||||
std::atomic<float>::is_always_lock_free &&
|
||||
std::atomic<double>::is_always_lock_free &&
|
||||
std::atomic<bool>::is_always_lock_free,
|
||||
"Network statistics require lock-free scalar atomics");
|
||||
|
||||
public:
|
||||
void Store(const MiniRtcNetTrafficStats& stats) {
|
||||
video_inbound_stats_.Store(stats.video_inbound_stats);
|
||||
video_outbound_stats_.Store(stats.video_outbound_stats);
|
||||
audio_inbound_stats_.Store(stats.audio_inbound_stats);
|
||||
audio_outbound_stats_.Store(stats.audio_outbound_stats);
|
||||
data_inbound_stats_.Store(stats.data_inbound_stats);
|
||||
data_outbound_stats_.Store(stats.data_outbound_stats);
|
||||
total_inbound_stats_.Store(stats.total_inbound_stats);
|
||||
total_outbound_stats_.Store(stats.total_outbound_stats);
|
||||
srtp_active_.store(stats.srtp_active, std::memory_order_relaxed);
|
||||
rtt_ms_.store(stats.rtt_ms, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
MiniRtcNetTrafficStats Load() const {
|
||||
MiniRtcNetTrafficStats stats{};
|
||||
stats.video_inbound_stats = video_inbound_stats_.Load();
|
||||
stats.video_outbound_stats = video_outbound_stats_.Load();
|
||||
stats.audio_inbound_stats = audio_inbound_stats_.Load();
|
||||
stats.audio_outbound_stats = audio_outbound_stats_.Load();
|
||||
stats.data_inbound_stats = data_inbound_stats_.Load();
|
||||
stats.data_outbound_stats = data_outbound_stats_.Load();
|
||||
stats.total_inbound_stats = total_inbound_stats_.Load();
|
||||
stats.total_outbound_stats = total_outbound_stats_.Load();
|
||||
stats.srtp_active = srtp_active_.load(std::memory_order_relaxed);
|
||||
stats.rtt_ms = rtt_ms_.load(std::memory_order_relaxed);
|
||||
return stats;
|
||||
}
|
||||
|
||||
uint32_t DataOutboundBitrate() const {
|
||||
return data_outbound_stats_.bitrate.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Reset() { Store(MiniRtcNetTrafficStats{}); }
|
||||
|
||||
private:
|
||||
struct InboundStats {
|
||||
void Store(const MiniRtcInboundStats& stats) {
|
||||
bitrate.store(stats.bitrate, std::memory_order_relaxed);
|
||||
rtp_packet_count.store(stats.rtp_packet_count, std::memory_order_relaxed);
|
||||
loss_rate.store(stats.loss_rate, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
MiniRtcInboundStats Load() const {
|
||||
return {bitrate.load(std::memory_order_relaxed),
|
||||
rtp_packet_count.load(std::memory_order_relaxed),
|
||||
loss_rate.load(std::memory_order_relaxed)};
|
||||
}
|
||||
|
||||
std::atomic<uint32_t> bitrate{0};
|
||||
std::atomic<uint32_t> rtp_packet_count{0};
|
||||
std::atomic<float> loss_rate{0};
|
||||
};
|
||||
|
||||
struct OutboundStats {
|
||||
void Store(const MiniRtcOutboundStats& stats) {
|
||||
bitrate.store(stats.bitrate, std::memory_order_relaxed);
|
||||
rtp_packet_count.store(stats.rtp_packet_count, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
MiniRtcOutboundStats Load() const {
|
||||
return {bitrate.load(std::memory_order_relaxed),
|
||||
rtp_packet_count.load(std::memory_order_relaxed)};
|
||||
}
|
||||
|
||||
std::atomic<uint32_t> bitrate{0};
|
||||
std::atomic<uint32_t> rtp_packet_count{0};
|
||||
};
|
||||
|
||||
InboundStats video_inbound_stats_;
|
||||
OutboundStats video_outbound_stats_;
|
||||
InboundStats audio_inbound_stats_;
|
||||
OutboundStats audio_outbound_stats_;
|
||||
InboundStats data_inbound_stats_;
|
||||
OutboundStats data_outbound_stats_;
|
||||
InboundStats total_inbound_stats_;
|
||||
OutboundStats total_outbound_stats_;
|
||||
std::atomic<bool> srtp_active_{false};
|
||||
std::atomic<double> rtt_ms_{-1};
|
||||
};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif
|
||||
@@ -300,6 +300,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
|
||||
"connection_closed");
|
||||
props->connection_established_ = false;
|
||||
props->net_traffic_stats_.Reset();
|
||||
props->enable_mouse_control_ = false;
|
||||
runtime->ResetRemoteServiceStatus(*props);
|
||||
{
|
||||
@@ -520,7 +521,7 @@ void PeerEventHandler::OnNetStatusReport(
|
||||
|
||||
// only display client side net status if connected to itself
|
||||
if (!(runtime->peer_reserved_ && !strstr(client_id, "C-"))) {
|
||||
props->net_traffic_stats_ = *net_traffic_stats;
|
||||
props->net_traffic_stats_.Store(*net_traffic_stats);
|
||||
}
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "cursor_presentation.h"
|
||||
#include "minirtc.h"
|
||||
#include "rendering/video_latency.h"
|
||||
#include "runtime/net_traffic_stats_cache.h"
|
||||
|
||||
namespace crossdesk { class PeerEventHandler; }
|
||||
|
||||
@@ -157,7 +158,7 @@ struct RemoteSession {
|
||||
std::chrono::steady_clock::time_point last_time_;
|
||||
// UI-thread snapshot refreshed with FPS once per second.
|
||||
std::optional<VideoLatencyStats::Snapshot> video_latency_snapshot_;
|
||||
MiniRtcNetTrafficStats net_traffic_stats_{};
|
||||
NetTrafficStatsCache net_traffic_stats_;
|
||||
|
||||
using QueuedFile = FileTransferState::QueuedFile;
|
||||
using FileTransferStatus = FileTransferState::FileTransferStatus;
|
||||
|
||||
@@ -57,6 +57,7 @@ export global StreamStrings {
|
||||
in-out property <string> stats-loss-rate: "Loss Rate";
|
||||
in-out property <string> stats-resolution: "Res";
|
||||
in-out property <string> stats-video-latency: "Video delay";
|
||||
in-out property <string> stats-connection-latency: "Connection RTT";
|
||||
in-out property <string> stats-connection-mode: "Mode";
|
||||
in-out property <string> stats-encryption: "Encryption";
|
||||
in-out property <string> stats-encryption-enabled: "Enabled";
|
||||
@@ -220,6 +221,7 @@ export component StreamWindow inherits Window {
|
||||
in property <string> stats-fps: "0";
|
||||
in property <string> stats-resolution: "";
|
||||
in property <string> stats-video-latency: "—";
|
||||
in property <string> stats-connection-latency: "—";
|
||||
in property <string> stats-connection-mode: "";
|
||||
in property <bool> stats-encryption-enabled: false;
|
||||
in property <bool> file-transfer-visible: false;
|
||||
@@ -905,18 +907,22 @@ export component StreamWindow inherits Window {
|
||||
private property <length> details-right-width:
|
||||
right-label-width-probe.preferred-width + 6px
|
||||
+ max(resolution-width-probe.preferred-width,
|
||||
connection-latency-width-probe.preferred-width,
|
||||
encryption-width-probe.preferred-width
|
||||
+ (root.stats-encryption-enabled ? 14px : 0px));
|
||||
private property <length> details-right-x: self.width / 2;
|
||||
private property <length> details-right-x:
|
||||
min(self.width / 2, self.width - self.details-right-width - 6px);
|
||||
|
||||
left-label-width-probe := Text {
|
||||
visible: false;
|
||||
text: "FPS\n" + StreamStrings.stats-connection-mode;
|
||||
text: "FPS\n" + StreamStrings.stats-video-latency
|
||||
+ "\n" + StreamStrings.stats-connection-mode;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
}
|
||||
right-label-width-probe := Text {
|
||||
visible: false;
|
||||
text: StreamStrings.stats-resolution + "\n" + StreamStrings.stats-encryption;
|
||||
text: StreamStrings.stats-resolution + "\n" + StreamStrings.stats-connection-latency
|
||||
+ "\n" + StreamStrings.stats-encryption;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
}
|
||||
resolution-width-probe := Text {
|
||||
@@ -925,6 +931,12 @@ export component StreamWindow inherits Window {
|
||||
font-size: ImGuiFontStyle.body;
|
||||
font-weight: 600;
|
||||
}
|
||||
connection-latency-width-probe := Text {
|
||||
visible: false;
|
||||
text: root.stats-connection-latency;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
font-weight: 600;
|
||||
}
|
||||
encryption-width-probe := Text {
|
||||
visible: false;
|
||||
text: root.stats-encryption-enabled
|
||||
@@ -1028,38 +1040,17 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
x: 6px;
|
||||
y: root.stats-header-height + root.stats-rows.length * stats-table.row-height + 44px;
|
||||
width: parent.width - 12px;
|
||||
height: 21px;
|
||||
spacing: 6px;
|
||||
Text {
|
||||
text: StreamStrings.stats-video-latency;
|
||||
color: #667085;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
text: root.stats-video-latency;
|
||||
color: #253247;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
font-weight: 600;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
|
||||
for metric[index] in [
|
||||
{ label: "FPS", value: root.stats-fps },
|
||||
{ label: StreamStrings.stats-resolution, value: root.stats-resolution },
|
||||
{ label: StreamStrings.stats-video-latency, value: root.stats-video-latency },
|
||||
{ label: StreamStrings.stats-connection-latency, value: root.stats-connection-latency },
|
||||
{ label: StreamStrings.stats-connection-mode, value: root.stats-connection-mode },
|
||||
{ label: StreamStrings.stats-encryption, value: root.stats-encryption-enabled
|
||||
? StreamStrings.stats-encryption-enabled : StreamStrings.stats-encryption-disabled },
|
||||
]: Rectangle {
|
||||
private property <length> encryption-icon-width:
|
||||
index == 3 && root.stats-encryption-enabled ? 14px : 0px;
|
||||
index == 5 && root.stats-encryption-enabled ? 14px : 0px;
|
||||
x: mod(index, 2) == 1 ? stats-table.details-right-x : 6px;
|
||||
y: root.stats-header-height + root.stats-rows.length * stats-table.row-height
|
||||
+ 2px + floor(index / 2) * 21px;
|
||||
@@ -1083,14 +1074,14 @@ export component StreamWindow inherits Window {
|
||||
height: parent.height;
|
||||
width: parent.width - self.x - parent.encryption-icon-width;
|
||||
text: metric.value;
|
||||
color: index == 3 && root.stats-encryption-enabled ? #18794e : #253247;
|
||||
color: index == 5 && root.stats-encryption-enabled ? #18794e : #253247;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
font-weight: 600;
|
||||
horizontal-alignment: left;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
if index == 3 && root.stats-encryption-enabled: Text {
|
||||
if index == 5 && root.stats-encryption-enabled: Text {
|
||||
x: metric-value.x + min(metric-value.preferred-width, metric-value.width) + 4px;
|
||||
y: metric-value.y + (metric-value.height - self.height) / 2;
|
||||
width: 10px;
|
||||
|
||||
@@ -176,6 +176,7 @@ inline int ApplyStreamWindowStrings(
|
||||
strings.set_stats_loss_rate(Text(localization::loss_rate[language]));
|
||||
strings.set_stats_resolution(Text(localization::resolution[language]));
|
||||
strings.set_stats_video_latency(Text(localization::video_latency[language]));
|
||||
strings.set_stats_connection_latency(Text(localization::connection_latency[language]));
|
||||
strings.set_stats_connection_mode(
|
||||
Text(localization::connection_mode[language]));
|
||||
strings.set_stats_encryption(
|
||||
|
||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: a25eff6515...77cabf43cb
Reference in New Issue
Block a user