mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-25 22:56:23 +08:00
[feat] show video latency in DeskHub network status
This commit is contained in:
@@ -5222,7 +5222,17 @@ void GuiApplication::SyncGallery() {
|
||||
: mode == TraversalMode::UnknownMode
|
||||
? tr("等待统计", "Awaiting stats", "Ждём данные")
|
||||
: tr("中继", "Relay", "Ретрансляция"));
|
||||
device.latency = device.inbound = device.outbound = device.loss = "—";
|
||||
device.inbound = device.outbound = device.loss = "—";
|
||||
device.video_latency = "—";
|
||||
// Prefer the wall's own frame path. An independently displayed stream
|
||||
// may still supply a measurement for a device outside the current page.
|
||||
auto video_latency = props->gallery_video_latency_->Get(now);
|
||||
if (!video_latency) video_latency = props->video_latency_->Get(now);
|
||||
if (video_latency) {
|
||||
const auto ms = video_latency->average_ms;
|
||||
device.video_latency = UiText(ms < 1 ? "<1 ms"
|
||||
: std::to_string(static_cast<int>(std::lround(ms))) + " ms");
|
||||
}
|
||||
const auto video = props->received_video_stats_.Get(now);
|
||||
device.resolution = video.width > 0 && video.height > 0
|
||||
? UiText(std::to_string(video.width) + " × " + std::to_string(video.height))
|
||||
@@ -5237,9 +5247,6 @@ void GuiApplication::SyncGallery() {
|
||||
if (mode != TraversalMode::UnknownMode && has_report) {
|
||||
device.inbound = UiText(FormatBitrate(stats.total_inbound_stats.bitrate));
|
||||
device.outbound = UiText(FormatBitrate(stats.total_outbound_stats.bitrate));
|
||||
if (const auto rtt = props->net_traffic_stats_.SmoothedRttMs(now))
|
||||
device.latency = UiText(*rtt < 1 ? "<1 ms"
|
||||
: std::to_string(static_cast<int>(std::lround(*rtt))) + " ms");
|
||||
// MiniRTC's total loss is a sum of media fractions, not an aggregate.
|
||||
// Report video loss explicitly, and only after receiving video packets.
|
||||
if (stats.video_inbound_stats.rtp_packet_count > 0 &&
|
||||
@@ -5447,6 +5454,7 @@ void GuiApplication::SyncGallery() {
|
||||
: gallery::PreviewResolution{};
|
||||
std::set<std::string> preview_ids;
|
||||
std::vector<ui::GalleryTile> tiles;
|
||||
std::vector<VideoLatencyFrame> submitted_previews;
|
||||
const auto make_tile = [&](const std::string& id, bool preview) {
|
||||
ui::GalleryTile tile{};
|
||||
tile.id = UiText(id);
|
||||
@@ -5504,6 +5512,7 @@ void GuiApplication::SyncGallery() {
|
||||
cached.image = slint::Image(std::move(pixels));
|
||||
cached.sequence = props->gallery_sequence_;
|
||||
cached.session = props;
|
||||
submitted_previews.push_back(props->gallery_frame_timing_);
|
||||
}
|
||||
tile.frame = cached.image;
|
||||
tile.has_frame = true;
|
||||
@@ -5535,6 +5544,9 @@ void GuiApplication::SyncGallery() {
|
||||
ui_->wall->set_gallery_focused(ui_->gallery_focus.empty()
|
||||
? ui::GalleryTile{}
|
||||
: make_tile(ui_->gallery_focus, true));
|
||||
// Count each newly submitted image once, including conversion and UI queue
|
||||
// time. Slint image submission does not confirm physical screen presentation.
|
||||
for (const auto& timing : submitted_previews) timing.MarkSubmitted();
|
||||
ui_->wall->set_gallery_connected(connected);
|
||||
if (!ui_->wall->get_gallery_adding() && !ui_->wall->get_gallery_importing())
|
||||
ui_->wall->set_gallery_notice("");
|
||||
|
||||
@@ -510,6 +510,11 @@ void GuiRuntime::ResetRemoteSessionResources(
|
||||
std::shared_ptr<RemoteSession> props) {
|
||||
props->net_traffic_stats_.Reset();
|
||||
props->received_video_stats_.Reset();
|
||||
{
|
||||
std::lock_guard lock(props->gallery_frame_mutex_);
|
||||
props->gallery_video_latency_->Reset();
|
||||
props->gallery_frame_timing_ = {};
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->video_latency_->Reset();
|
||||
|
||||
@@ -331,6 +331,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
}
|
||||
props->net_traffic_stats_.Reset();
|
||||
props->received_video_stats_.Reset();
|
||||
{
|
||||
std::lock_guard lock(props->gallery_frame_mutex_);
|
||||
props->gallery_video_latency_->Reset();
|
||||
props->gallery_frame_timing_ = {};
|
||||
}
|
||||
props->traversal_mode_ = TraversalMode::UnknownMode;
|
||||
props->enable_mouse_control_ = false;
|
||||
props->preview_subscription_supported_ = false;
|
||||
|
||||
@@ -50,6 +50,8 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
|
||||
if (props->connection_established_) {
|
||||
const auto timing_now = VideoLatencyFrame::Clock::now();
|
||||
// Pair the clocks before preview conversion so its cost is included.
|
||||
const auto local_now_us = GetSystemTimeMicros(props->peer_);
|
||||
const auto* received_native = GetNativeVideoFrame(video_frame);
|
||||
props->received_video_stats_.Record(
|
||||
received_native ? received_native->width : video_frame->width,
|
||||
@@ -65,6 +67,8 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
std::defer_lock);
|
||||
if (preview_width > 0) conversion_lock.lock();
|
||||
if (conversion_lock.owns_lock()) {
|
||||
const auto gallery_timing = props->gallery_video_latency_->Frame(
|
||||
video_frame->captured_timestamp, local_now_us, timing_now);
|
||||
const auto* native = GetNativeVideoFrame(video_frame);
|
||||
const int w = native ? native->width : video_frame->width;
|
||||
const int h = native ? native->height : video_frame->height;
|
||||
@@ -100,6 +104,7 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
props->gallery_width_ = dw;
|
||||
props->gallery_height_ = dh;
|
||||
props->gallery_frame_at_ = timing_now;
|
||||
props->gallery_frame_timing_ = gallery_timing;
|
||||
++props->gallery_sequence_;
|
||||
}
|
||||
}
|
||||
@@ -118,8 +123,7 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
return;
|
||||
}
|
||||
const auto timing = props->video_latency_->Frame(
|
||||
video_frame->captured_timestamp, GetSystemTimeMicros(props->peer_),
|
||||
timing_now);
|
||||
video_frame->captured_timestamp, local_now_us, timing_now);
|
||||
bool background_snapshot_only = false;
|
||||
auto* native_renderer = runtime->video_renderer_.get();
|
||||
if (native_renderer && native_renderer->IsActive()) {
|
||||
|
||||
@@ -95,6 +95,10 @@ struct RemoteSession {
|
||||
int gallery_width_ = 0, gallery_height_ = 0;
|
||||
uint64_t gallery_sequence_ = 0;
|
||||
std::chrono::steady_clock::time_point gallery_frame_at_{};
|
||||
VideoLatencyFrame gallery_frame_timing_;
|
||||
// Keep preview submission estimates separate from stream presentation times.
|
||||
std::shared_ptr<VideoLatencyStats> gallery_video_latency_ =
|
||||
std::make_shared<VideoLatencyStats>();
|
||||
// Decoder-thread scratch memory, never shared with the UI.
|
||||
std::mutex gallery_conversion_mutex_;
|
||||
std::vector<unsigned char> gallery_nv12_, gallery_scaled_, gallery_scratch_;
|
||||
|
||||
@@ -8,7 +8,7 @@ export struct GalleryNetworkDevice {
|
||||
mode: string,
|
||||
encryption-known: bool,
|
||||
encryption-enabled: bool,
|
||||
latency: string,
|
||||
video-latency: string,
|
||||
inbound: string,
|
||||
outbound: string,
|
||||
loss: string,
|
||||
@@ -122,8 +122,8 @@ component NetworkRow inherits Rectangle {
|
||||
Text {
|
||||
y: root.heading ? 0px : 8px; height: root.heading ? parent.height : 20px;
|
||||
width: parent.width;
|
||||
text: root.heading ? GalleryStrings.tr("网络质量", "Network quality", "Качество сети") : "RTT " + root.device.latency;
|
||||
font-size: 11px; font-weight: root.heading ? 600 : 400;
|
||||
text: root.heading ? GalleryStrings.tr("视频延时", "Video latency", "Задержка видео") : root.device.video-latency;
|
||||
font-size: root.heading ? 11px : 12px; font-weight: root.heading ? 600 : 400;
|
||||
color: root.heading ? #6b7785 : #293746;
|
||||
vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
@@ -191,14 +191,10 @@ export component GalleryNetworkDialog inherits Rectangle {
|
||||
x: parent.width - 80px; y: 17px; width: 60px; height: 26px;
|
||||
text: GalleryStrings.tr("关闭", "Close", "Закрыть"); clicked => { root.close(); }
|
||||
}
|
||||
Rectangle {
|
||||
x: 20px; y: 54px; width: 6px; height: 6px; border-radius: 3px;
|
||||
background: root.devices.length > 0 ? #27a168 : #a7b0ba;
|
||||
}
|
||||
Text {
|
||||
x: 32px; y: 48px; width: parent.width - 52px; height: 18px;
|
||||
text: GalleryStrings.tr("已连接设备 · ", "Connected devices · ", "Подключённые устройства · ") + root.devices.length
|
||||
+ GalleryStrings.tr(" 台 · 每秒更新", " · Updates every second", " · Обновление раз в секунду");
|
||||
x: 20px; y: 48px; width: parent.width - 40px; height: 18px;
|
||||
text: GalleryStrings.tr("已连接设备 ", "Connected devices ", "Подключённые устройства ") + root.devices.length
|
||||
+ GalleryStrings.tr(" 台", "", "");
|
||||
font-size: 11px; color: #7a8490; overflow: elide;
|
||||
}
|
||||
NetworkRow {
|
||||
|
||||
Reference in New Issue
Block a user