mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-24 13:48:51 +08:00
[feat] display video codecs and refine network statistics
This commit is contained in:
@@ -473,7 +473,7 @@ std::string FormatBitrate(uint32_t bits_per_second) {
|
||||
} else if (bits_per_second < 1000000) {
|
||||
std::snprintf(text, sizeof(text), "%u kbps", bits_per_second / 1000);
|
||||
} else {
|
||||
std::snprintf(text, sizeof(text), "%.1f mbps", bits_per_second / 1000000.0);
|
||||
std::snprintf(text, sizeof(text), "%.1f Mbps", bits_per_second / 1000000.0);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -3636,6 +3636,13 @@ void GuiApplication::SyncStreamWindow() {
|
||||
(*ui_->stream)->set_selected_display(props->selected_display_);
|
||||
|
||||
const auto net = props->net_traffic_stats_.Load();
|
||||
const auto codecs = props->net_traffic_stats_.LoadVideoCodecs();
|
||||
(*ui_->stream)->set_stats_encoder(UiText(
|
||||
status == ConnectionStatus::Connected && !codecs.remote_encoder.empty()
|
||||
? codecs.remote_encoder : "—"));
|
||||
(*ui_->stream)->set_stats_decoder(UiText(
|
||||
status == ConnectionStatus::Connected && !codecs.local_decoder.empty()
|
||||
? codecs.local_decoder : "—"));
|
||||
(*ui_->stream)->set_stats_encryption_enabled(
|
||||
status == ConnectionStatus::Connected && net.srtp_active);
|
||||
std::vector<ui::NetworkStatsRow> stats_rows;
|
||||
@@ -3659,7 +3666,7 @@ void GuiApplication::SyncStreamWindow() {
|
||||
net.total_inbound_stats, net.total_outbound_stats);
|
||||
ui::UpdateModelRows(*ui_->stats_model, std::move(stats_rows));
|
||||
(*ui_->stream)
|
||||
->set_stats_resolution(UiText(std::to_string(props->video_width_) + "x" +
|
||||
->set_stats_resolution(UiText(std::to_string(props->video_width_) + " × " +
|
||||
std::to_string(props->video_height_)));
|
||||
(*ui_->stream)
|
||||
->set_stats_connection_mode(
|
||||
@@ -5738,6 +5745,9 @@ void GuiApplication::SyncGallery() {
|
||||
? UiText(std::to_string(video.width) + " × " + std::to_string(video.height))
|
||||
: UiText("—");
|
||||
device.fps = video.fps ? UiText(std::to_string(*video.fps) + " FPS") : UiText("—");
|
||||
const auto codecs = props->net_traffic_stats_.LoadVideoCodecs();
|
||||
device.encoder = UiText(codecs.remote_encoder.empty() ? "—" : codecs.remote_encoder);
|
||||
device.decoder = UiText(codecs.local_decoder.empty() ? "—" : codecs.local_decoder);
|
||||
const bool has_report = props->net_traffic_stats_.HasRecentReport(now);
|
||||
const auto stats = props->net_traffic_stats_.Load();
|
||||
// Use negotiated transport state, never the local encryption preference.
|
||||
|
||||
@@ -372,6 +372,8 @@ struct TranslationRow {
|
||||
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
|
||||
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
|
||||
X(transport_encryption, u8"加密传输", "Encryption", u8"Шифрование") \
|
||||
X(remote_video_encoder, u8"被控端编码器", "Remote encoder", u8"Кодер удалённой стороны") \
|
||||
X(local_video_decoder, u8"控制端解码器", "Local decoder", u8"Локальный декодер") \
|
||||
X(transport_encryption_enabled, u8"已开启", "Enabled", u8"Включено") \
|
||||
X(transport_encryption_disabled, u8"未开启", "Disabled", u8"Выключено") \
|
||||
X(online, u8"在线", "Online", u8"Онлайн") \
|
||||
|
||||
@@ -199,6 +199,7 @@ void GuiRuntime::HandleServerControllerDisconnected(
|
||||
remote_input_.Disconnect(remote_id);
|
||||
devices_.OnControllerConnectionsChanged();
|
||||
connection_host_names_.erase(remote_id);
|
||||
codec_status_controllers_.erase(remote_id);
|
||||
privacy_sessions_.Disconnected(remote_id);
|
||||
for (const auto& [id, status] : connection_status_) {
|
||||
if (status != ConnectionStatus::Connected) {
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#define _NET_TRAFFIC_STATS_CACHE_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "minirtc.h"
|
||||
#include "runtime/connection_latency.h"
|
||||
@@ -15,7 +18,8 @@
|
||||
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.
|
||||
// Relaxed accesses keep scalar fields safe without publishing other state;
|
||||
// codec names are copied under their own mutex.
|
||||
class NetTrafficStatsCache {
|
||||
static_assert(std::atomic<uint32_t>::is_always_lock_free &&
|
||||
std::atomic<float>::is_always_lock_free &&
|
||||
@@ -26,7 +30,31 @@ class NetTrafficStatsCache {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
struct VideoCodecNames {
|
||||
std::string remote_encoder;
|
||||
std::string local_decoder;
|
||||
};
|
||||
|
||||
void StoreRemoteVideoEncoder(const std::string& encoder) {
|
||||
std::lock_guard lock(codec_mutex_);
|
||||
codecs_.remote_encoder = encoder;
|
||||
}
|
||||
|
||||
VideoCodecNames LoadVideoCodecs() const {
|
||||
std::lock_guard lock(codec_mutex_);
|
||||
return codecs_;
|
||||
}
|
||||
|
||||
void Store(const MiniRtcNetTrafficStats& stats) {
|
||||
{
|
||||
std::lock_guard lock(codec_mutex_);
|
||||
local_encoder_.assign(
|
||||
stats.video_encoder,
|
||||
strnlen(stats.video_encoder, sizeof(stats.video_encoder)));
|
||||
codecs_.local_decoder.assign(
|
||||
stats.video_decoder,
|
||||
strnlen(stats.video_decoder, sizeof(stats.video_decoder)));
|
||||
}
|
||||
video_inbound_stats_.Store(stats.video_inbound_stats);
|
||||
video_outbound_stats_.Store(stats.video_outbound_stats);
|
||||
audio_inbound_stats_.Store(stats.audio_inbound_stats);
|
||||
@@ -46,6 +74,11 @@ class NetTrafficStatsCache {
|
||||
|
||||
MiniRtcNetTrafficStats Load() const {
|
||||
MiniRtcNetTrafficStats stats{};
|
||||
{
|
||||
std::lock_guard lock(codec_mutex_);
|
||||
local_encoder_.copy(stats.video_encoder, sizeof(stats.video_encoder) - 1);
|
||||
codecs_.local_decoder.copy(stats.video_decoder, sizeof(stats.video_decoder) - 1);
|
||||
}
|
||||
stats.video_inbound_stats = video_inbound_stats_.Load();
|
||||
stats.video_outbound_stats = video_outbound_stats_.Load();
|
||||
stats.audio_inbound_stats = audio_inbound_stats_.Load();
|
||||
@@ -80,6 +113,10 @@ class NetTrafficStatsCache {
|
||||
|
||||
void Reset() {
|
||||
Store(MiniRtcNetTrafficStats{});
|
||||
{
|
||||
std::lock_guard lock(codec_mutex_);
|
||||
codecs_ = {};
|
||||
}
|
||||
ResetLatency();
|
||||
last_report_ms_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
@@ -130,6 +167,9 @@ class NetTrafficStatsCache {
|
||||
std::atomic<double> rtt_ms_{-1};
|
||||
std::atomic<int64_t> last_report_ms_{0};
|
||||
ConnectionLatencyStats connection_latency_;
|
||||
mutable std::mutex codec_mutex_;
|
||||
std::string local_encoder_;
|
||||
VideoCodecNames codecs_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
@@ -209,6 +209,16 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
return;
|
||||
}
|
||||
|
||||
if (remote_action.type == ControlType::video_codec_status) {
|
||||
if (auto props = runtime->FindRemoteSession(remote_id);
|
||||
props && source_id == props->control_data_label_ &&
|
||||
!props->closing_ &&
|
||||
props->connection_status_ == ConnectionStatus::Connected) {
|
||||
props->net_traffic_stats_.StoreRemoteVideoEncoder(remote_action.vcs.encoder);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (remote_action.type == ControlType::video_settings_status) {
|
||||
if (auto props = runtime->FindRemoteSession(remote_id)) {
|
||||
if (source_id != props->control_data_label_) return;
|
||||
@@ -426,6 +436,12 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
{
|
||||
std::unique_lock lock(runtime->connection_status_mutex_);
|
||||
runtime->connection_host_names_[remote_id] = host_name;
|
||||
if (source_id == runtime->control_data_label_) {
|
||||
if (remote_action.i.supports_video_codec_status)
|
||||
runtime->codec_status_controllers_.insert(remote_id);
|
||||
else
|
||||
runtime->codec_status_controllers_.erase(remote_id);
|
||||
}
|
||||
}
|
||||
LOG_INFO("Remote hostname: [{}]", host_name);
|
||||
if (source_id == runtime->control_data_label_) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
@@ -295,6 +296,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
/*supports_privacy_screen=*/!runtime->unattended_host_,
|
||||
CROSSDESK_VERSION, kClientPlatform);
|
||||
remote_action.i.supports_video_settings = true;
|
||||
remote_action.i.supports_video_codec_status = true;
|
||||
remote_action.i.supports_text_input = runtime->devices_.SupportsTextInput();
|
||||
remote_action.i.supports_deskhub_preview = true;
|
||||
remote_action.i.supports_deskhub_resolution = true;
|
||||
@@ -460,6 +462,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
/*supports_privacy_screen=*/!runtime->unattended_host_,
|
||||
CROSSDESK_VERSION, kClientPlatform);
|
||||
remote_action.i.supports_video_settings = true;
|
||||
remote_action.i.supports_video_codec_status = true;
|
||||
remote_action.i.supports_text_input = runtime->devices_.SupportsTextInput();
|
||||
remote_action.i.supports_deskhub_preview = true;
|
||||
remote_action.i.supports_deskhub_resolution = true;
|
||||
@@ -577,7 +580,29 @@ void PeerEventHandler::OnNetStatusReport(
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
auto props = runtime->FindRemoteSession(remote_id);
|
||||
if (!props) return;
|
||||
if (!props) {
|
||||
bool report_codecs = false;
|
||||
{
|
||||
std::shared_lock lock(runtime->connection_status_mutex_);
|
||||
const auto status = runtime->connection_status_.find(remote_id);
|
||||
report_codecs = runtime->codec_status_controllers_.count(remote_id) != 0 &&
|
||||
status != runtime->connection_status_.end() &&
|
||||
status->second == ConnectionStatus::Connected;
|
||||
}
|
||||
if (net_traffic_stats && report_codecs) {
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::video_codec_status;
|
||||
std::snprintf(action.vcs.encoder, sizeof(action.vcs.encoder), "%.*s",
|
||||
static_cast<int>(sizeof(net_traffic_stats->video_encoder)),
|
||||
net_traffic_stats->video_encoder);
|
||||
const auto message = action.to_json();
|
||||
// Report only to this controller, after releasing the status lock.
|
||||
SendReliableDataFrameToPeer(runtime->peer_, message.data(), message.size(),
|
||||
runtime->control_data_label_.c_str(),
|
||||
remote_id.data(), remote_id.size());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (props->traversal_mode_ != mode) {
|
||||
props->net_traffic_stats_.ResetLatency();
|
||||
props->traversal_mode_ = mode;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -206,6 +207,7 @@ struct ConnectionState {
|
||||
std::shared_mutex connection_status_mutex_;
|
||||
std::unordered_map<std::string, ConnectionStatus> connection_status_;
|
||||
std::unordered_map<std::string, std::string> connection_host_names_;
|
||||
std::unordered_set<std::string> codec_status_controllers_;
|
||||
// Cursor position is sampled asynchronously after remote mouse input has
|
||||
// been injected. Remember the input source so the sampled position is not
|
||||
// immediately echoed back to that same controller as stale feedback.
|
||||
|
||||
@@ -15,6 +15,8 @@ export struct GalleryNetworkDevice {
|
||||
loss: string,
|
||||
resolution: string,
|
||||
fps: string,
|
||||
encoder: string,
|
||||
decoder: string,
|
||||
}
|
||||
|
||||
component NetworkRow inherits Rectangle {
|
||||
@@ -145,6 +147,26 @@ component NetworkRow inherits Rectangle {
|
||||
vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
}
|
||||
if !root.heading: HorizontalLayout {
|
||||
x: 12px; y: 52px; width: parent.width - 24px; height: 20px;
|
||||
spacing: 12px;
|
||||
Rectangle {
|
||||
width: (parent.width - 12px) / 2;
|
||||
Text {
|
||||
width: parent.width; height: parent.height;
|
||||
text: GalleryStrings.tr("被控端编码器 ", "Remote encoder ", "Удалённый кодер ") + root.device.encoder;
|
||||
font-size: 10px; color: #586f87; vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: (parent.width - 12px) / 2;
|
||||
Text {
|
||||
width: parent.width; height: parent.height;
|
||||
text: GalleryStrings.tr("控制端解码器 ", "Local decoder ", "Локальный декодер ") + root.device.decoder;
|
||||
font-size: 10px; color: #586f87; vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component NetworkEmptyState inherits Rectangle {
|
||||
@@ -163,7 +185,7 @@ component NetworkEmptyState inherits Rectangle {
|
||||
|
||||
export component GalleryNetworkDialog inherits Rectangle {
|
||||
in property <[GalleryNetworkDevice]> devices;
|
||||
private property <length> row-height: 56px;
|
||||
private property <length> row-height: 78px;
|
||||
private property <length> list-height: max(2, min(root.devices.length, 6)) * root.row-height;
|
||||
callback close();
|
||||
background: #00000042;
|
||||
|
||||
@@ -64,6 +64,8 @@ export global StreamStrings {
|
||||
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-encoder: "Remote encoder";
|
||||
in-out property <string> stats-decoder: "Local decoder";
|
||||
in-out property <string> stats-encryption: "Encryption";
|
||||
in-out property <string> stats-encryption-enabled: "Enabled";
|
||||
in-out property <string> stats-encryption-disabled: "Disabled";
|
||||
@@ -230,6 +232,8 @@ export component StreamWindow inherits Window {
|
||||
in property <string> stats-video-latency: "—";
|
||||
in property <string> stats-connection-latency: "—";
|
||||
in property <string> stats-connection-mode: "";
|
||||
in property <string> stats-encoder: "—";
|
||||
in property <string> stats-decoder: "—";
|
||||
in property <bool> stats-encryption-enabled: false;
|
||||
in property <bool> file-transfer-visible: false;
|
||||
in property <[FileTransferEntry]> file-transfers;
|
||||
@@ -269,7 +273,7 @@ export component StreamWindow inherits Window {
|
||||
private property <length> stats-row-height: 18px;
|
||||
private property <length> stats-header-height: 18px;
|
||||
private property <length> stats-content-height:
|
||||
root.stats-header-height + root.stats-rows.length * root.stats-row-height + 65px;
|
||||
root.stats-header-height + root.stats-rows.length * root.stats-row-height + 114px;
|
||||
private property <length> file-panel-x: 18px;
|
||||
private property <length> file-panel-y: video.height - min(210px, video.height * 0.35);
|
||||
private property <length> file-panel-width: min(430px, video.width * 0.6);
|
||||
@@ -929,18 +933,17 @@ export component StreamWindow inherits Window {
|
||||
private property <length> row-height: root.stats-row-height;
|
||||
private property <length> label-column-width: 50px;
|
||||
private property <length> loss-column-width:
|
||||
max((self.width - self.label-column-width) / 3,
|
||||
stats-loss-label.preferred-width + 8px);
|
||||
max(56px, stats-loss-label.preferred-width + 12px);
|
||||
private property <length> value-column-width:
|
||||
(self.width - self.label-column-width - self.loss-column-width) / 2;
|
||||
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
|
||||
encryption-width-probe.preferred-width + 12px
|
||||
+ (root.stats-encryption-enabled ? 14px : 0px));
|
||||
private property <length> details-right-x:
|
||||
min(self.width / 2, self.width - self.details-right-width - 6px);
|
||||
min(self.width / 2 + 4px, self.width - self.details-right-width - 8px);
|
||||
|
||||
left-label-width-probe := Text {
|
||||
visible: false;
|
||||
@@ -970,7 +973,7 @@ export component StreamWindow inherits Window {
|
||||
visible: false;
|
||||
text: root.stats-encryption-enabled
|
||||
? StreamStrings.stats-encryption-enabled : StreamStrings.stats-encryption-disabled;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -979,7 +982,7 @@ export component StreamWindow inherits Window {
|
||||
y: 0px;
|
||||
width: parent.width;
|
||||
height: root.stats-header-height;
|
||||
background: #f3f5f8;
|
||||
background: #f5f7fa;
|
||||
border-radius: UiRadius.control;
|
||||
Text {
|
||||
x: stats-table.label-column-width + 4px;
|
||||
@@ -1016,13 +1019,13 @@ export component StreamWindow inherits Window {
|
||||
y: root.stats-header-height + index * stats-table.row-height;
|
||||
width: parent.width;
|
||||
height: stats-table.row-height;
|
||||
background: index == root.stats-rows.length - 1 ? #edf3fc : transparent;
|
||||
background: index == root.stats-rows.length - 1 ? #edf4ff : transparent;
|
||||
border-radius: UiRadius.control;
|
||||
Text {
|
||||
x: 4px;
|
||||
width: stats-table.label-column-width - 8px;
|
||||
text: row.label;
|
||||
color: #475467;
|
||||
color: index == root.stats-rows.length - 1 ? #275188 : #667085;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
|
||||
vertical-alignment: center;
|
||||
@@ -1031,7 +1034,7 @@ export component StreamWindow inherits Window {
|
||||
x: stats-table.label-column-width + 4px;
|
||||
width: stats-table.value-column-width - 8px;
|
||||
text: row.inbound;
|
||||
color: #253247;
|
||||
color: index == root.stats-rows.length - 1 ? #275188 : #344054;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
|
||||
horizontal-alignment: right;
|
||||
@@ -1042,7 +1045,7 @@ export component StreamWindow inherits Window {
|
||||
x: stats-table.label-column-width + stats-table.value-column-width + 4px;
|
||||
width: stats-table.value-column-width - 8px;
|
||||
text: row.outbound;
|
||||
color: #253247;
|
||||
color: index == root.stats-rows.length - 1 ? #275188 : #344054;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
|
||||
horizontal-alignment: right;
|
||||
@@ -1053,7 +1056,7 @@ export component StreamWindow inherits Window {
|
||||
x: stats-table.label-column-width + stats-table.value-column-width * 2 + 4px;
|
||||
width: stats-table.loss-column-width - 8px;
|
||||
text: row.loss-rate;
|
||||
color: #475467;
|
||||
color: index == root.stats-rows.length - 1 ? #275188 : #667085;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
font-weight: index == root.stats-rows.length - 1 ? 600 : 400;
|
||||
horizontal-alignment: right;
|
||||
@@ -1069,6 +1072,46 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
codec-label-width-probe := Text {
|
||||
visible: false;
|
||||
text: StreamStrings.stats-encoder + "\n" + StreamStrings.stats-decoder;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
}
|
||||
Rectangle {
|
||||
x: 0px;
|
||||
y: root.stats-header-height + root.stats-rows.length * stats-table.row-height
|
||||
+ 70px;
|
||||
width: parent.width; height: 44px;
|
||||
background: #f7f9fc; border-radius: UiRadius.control;
|
||||
border-width: 1px; border-color: #e8edf4;
|
||||
Rectangle {
|
||||
x: 8px; y: 22px; width: parent.width - 16px; height: 1px;
|
||||
background: #e8edf4;
|
||||
}
|
||||
for codec[index] in [
|
||||
{ label: StreamStrings.stats-encoder, value: root.stats-encoder },
|
||||
{ label: StreamStrings.stats-decoder, value: root.stats-decoder },
|
||||
]: Rectangle {
|
||||
x: 8px; y: 2px + index * 21px;
|
||||
width: parent.width - 16px; height: 19px;
|
||||
Text {
|
||||
x: 0px; y: 0px;
|
||||
width: codec-label-width-probe.preferred-width;
|
||||
height: parent.height; text: codec.label;
|
||||
color: #667085; font-size: ImGuiFontStyle.small;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
x: codec-label-width-probe.preferred-width + 8px; y: 0px;
|
||||
width: parent.width - self.x; height: parent.height;
|
||||
text: codec.value; color: #344054;
|
||||
font-size: ImGuiFontStyle.body; font-weight: 500;
|
||||
horizontal-alignment: right;
|
||||
vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for metric[index] in [
|
||||
{ label: "FPS", value: root.stats-fps },
|
||||
{ label: StreamStrings.stats-resolution, value: root.stats-resolution },
|
||||
@@ -1080,12 +1123,12 @@ export component StreamWindow inherits Window {
|
||||
]: Rectangle {
|
||||
private property <length> encryption-icon-width:
|
||||
index == 5 && root.stats-encryption-enabled ? 14px : 0px;
|
||||
x: mod(index, 2) == 1 ? stats-table.details-right-x : 6px;
|
||||
x: mod(index, 2) == 1 ? stats-table.details-right-x : 8px;
|
||||
y: root.stats-header-height + root.stats-rows.length * stats-table.row-height
|
||||
+ 2px + floor(index / 2) * 21px;
|
||||
width: mod(index, 2) == 1 ? stats-table.details-right-width
|
||||
: stats-table.details-right-x - 14px;
|
||||
height: 21px;
|
||||
+ 4px + floor(index / 2) * 20px;
|
||||
width: mod(index, 2) == 1 ? stats-table.width - stats-table.details-right-x - 8px
|
||||
: stats-table.details-right-x - 18px;
|
||||
height: 20px;
|
||||
metric-label := Text {
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
@@ -1097,31 +1140,40 @@ export component StreamWindow inherits Window {
|
||||
font-size: ImGuiFontStyle.small;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
metric-value := Text {
|
||||
x: metric-label.width + 6px;
|
||||
y: 0px;
|
||||
height: parent.height;
|
||||
width: parent.width - self.x - parent.encryption-icon-width;
|
||||
text: metric.value;
|
||||
color: index == 5 && root.stats-encryption-enabled ? #18794e : #253247;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
value-width-probe := Text {
|
||||
visible: false; text: metric.value;
|
||||
font-size: index >= 4 ? ImGuiFontStyle.small : ImGuiFontStyle.body;
|
||||
font-weight: 600;
|
||||
horizontal-alignment: left;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
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;
|
||||
height: 12px;
|
||||
text: FontAwesomeIcons.shield-halved;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: 10px;
|
||||
color: #238653;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
metric-value := Rectangle {
|
||||
private property <length> badge-padding: index >= 4 ? 6px : 0px;
|
||||
private property <length> icon-width: parent.encryption-icon-width;
|
||||
width: index >= 4
|
||||
? min(max(0px, parent.width - metric-label.width - 6px),
|
||||
value-width-probe.preferred-width + self.badge-padding * 2 + self.icon-width)
|
||||
: max(0px, parent.width - metric-label.width - 6px);
|
||||
x: parent.width - self.width; y: 2px; height: 16px;
|
||||
border-radius: 4px;
|
||||
background: index == 4 ? #edf3fc : index == 5
|
||||
? (root.stats-encryption-enabled ? #eaf6ee : #fff3e3) : transparent;
|
||||
Text {
|
||||
x: parent.badge-padding; y: 0px; height: parent.height;
|
||||
width: max(0px, parent.width - parent.badge-padding * 2 - parent.icon-width);
|
||||
text: metric.value;
|
||||
color: index == 4 ? #275188 : index == 5
|
||||
? (root.stats-encryption-enabled ? #18794e : #946015) : #253247;
|
||||
font-size: index >= 4 ? ImGuiFontStyle.small : ImGuiFontStyle.body;
|
||||
font-weight: 600; horizontal-alignment: right;
|
||||
vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
if index == 5 && root.stats-encryption-enabled: Text {
|
||||
x: parent.width - parent.badge-padding - 10px; y: 2px;
|
||||
width: 10px; height: 12px;
|
||||
text: FontAwesomeIcons.shield-halved;
|
||||
font-family: "Font Awesome 6 Free"; font-weight: 900;
|
||||
font-size: 10px; color: #238653;
|
||||
horizontal-alignment: center; vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +241,8 @@ inline int ApplyStreamWindowStrings(
|
||||
strings.set_stats_connection_latency(Text(localization::connection_latency[language]));
|
||||
strings.set_stats_connection_mode(
|
||||
Text(localization::connection_mode[language]));
|
||||
strings.set_stats_encoder(Text(localization::remote_video_encoder[language]));
|
||||
strings.set_stats_decoder(Text(localization::local_video_decoder[language]));
|
||||
strings.set_stats_encryption(
|
||||
Text(localization::transport_encryption[language]));
|
||||
strings.set_stats_encryption_enabled(
|
||||
|
||||
Reference in New Issue
Block a user