[fix] correct video latency measurement and smooth connection RTT

This commit is contained in:
dijunkun
2026-09-26 03:15:47 +08:00
parent 9ef64cc00f
commit bbbcfb9d6f
7 changed files with 108 additions and 6 deletions
@@ -2925,17 +2925,25 @@ void GuiApplication::SyncStreamWindow() {
static_cast<int>(std::lround(latency->average_ms))) + static_cast<int>(std::lround(latency->average_ms))) +
" ms" " ms"
: "—")); : "—"));
const auto display_rtt = props->net_traffic_stats_.SmoothedRttMs(fps_now);
const bool rtt_available = props->net_traffic_stats_button_pressed_ && const bool rtt_available = props->net_traffic_stats_button_pressed_ &&
status == ConnectionStatus::Connected && status == ConnectionStatus::Connected &&
net.rtt_ms >= 0; display_rtt.has_value();
std::string connection_latency = "—"; std::string connection_latency = "—";
if (rtt_available) { if (rtt_available) {
connection_latency = connection_latency =
net.rtt_ms < 1 *display_rtt < 1
? "<1 ms" ? "<1 ms"
: std::to_string(static_cast<int>(std::lround(net.rtt_ms))) + " ms"; : std::to_string(static_cast<int>(std::lround(*display_rtt))) + " ms";
} }
(*ui_->stream)->set_stats_connection_latency(UiText(connection_latency)); (*ui_->stream)->set_stats_connection_latency(UiText(connection_latency));
if (refresh_stats && status == ConnectionStatus::Connected) {
LOG_DEBUG("Latency stats: remote={} video_ms={} video_samples={} "
"rtt_raw_ms={} rtt_display_ms={}",
props->remote_id_, latency ? latency->average_ms : -1,
latency ? latency->samples : 0, net.rtt_ms,
display_rtt.value_or(-1));
}
} }
void GuiApplication::SyncStreamVideoFrame() { void GuiApplication::SyncStreamVideoFrame() {
@@ -26,6 +26,8 @@ struct VideoLatencyFrame {
std::shared_ptr<VideoLatencyStats> stats; std::shared_ptr<VideoLatencyStats> stats;
uint64_t id = 0; uint64_t id = 0;
Clock::time_point capture_time{}; Clock::time_point capture_time{};
void MarkPresented(Clock::time_point now = Clock::now()) const;
// Fallback for renderers which expose submission but no presentation time.
void MarkSubmitted(Clock::time_point now = Clock::now()) const; void MarkSubmitted(Clock::time_point now = Clock::now()) const;
}; };
@@ -97,6 +99,10 @@ class VideoLatencyStats
}; };
inline void VideoLatencyFrame::MarkSubmitted(Clock::time_point now) const { inline void VideoLatencyFrame::MarkSubmitted(Clock::time_point now) const {
MarkPresented(now);
}
inline void VideoLatencyFrame::MarkPresented(Clock::time_point now) const {
if (stats) stats->Record(*this, now); if (stats) stats->Record(*this, now);
} }
} // namespace crossdesk } // namespace crossdesk
@@ -0,0 +1,59 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CONNECTION_LATENCY_H_
#define _CONNECTION_LATENCY_H_
#include <chrono>
#include <cmath>
#include <mutex>
#include <optional>
namespace crossdesk::gui_detail {
// Presentation-only smoothing. The transport's raw RTT remains available for
// recovery, congestion control and diagnostics. A missing one-second report
// does not flicker, but a disconnected/stalled peer cannot leave a stale value.
class ConnectionLatencyStats {
public:
using Clock = std::chrono::steady_clock;
void Update(double rtt_ms, Clock::time_point now = Clock::now()) {
if (!std::isfinite(rtt_ms) || rtt_ms < 0 || rtt_ms > 2000) return;
std::lock_guard lock(mutex_);
if (average_ms_ && now < updated_) return;
if (!Fresh(now)) {
average_ms_ = rtt_ms;
} else {
*average_ms_ += 0.25 * (rtt_ms - *average_ms_);
}
updated_ = now;
}
std::optional<double> Get(Clock::time_point now = Clock::now()) const {
std::lock_guard lock(mutex_);
return Fresh(now) ? average_ms_ : std::nullopt;
}
void Reset() {
std::lock_guard lock(mutex_);
average_ms_.reset();
}
private:
bool Fresh(Clock::time_point now) const {
// The UI may snapshot `now` just before the report thread updates us.
// A newer valid report must not cause a one-frame unavailable flicker.
return average_ms_ && now - updated_ < std::chrono::seconds(3);
}
mutable std::mutex mutex_;
std::optional<double> average_ms_;
Clock::time_point updated_{};
};
} // namespace crossdesk::gui_detail
#endif
@@ -10,6 +10,7 @@
#include <atomic> #include <atomic>
#include "minirtc.h" #include "minirtc.h"
#include "runtime/connection_latency.h"
namespace crossdesk::gui_detail { namespace crossdesk::gui_detail {
@@ -34,6 +35,7 @@ class NetTrafficStatsCache {
total_outbound_stats_.Store(stats.total_outbound_stats); total_outbound_stats_.Store(stats.total_outbound_stats);
srtp_active_.store(stats.srtp_active, std::memory_order_relaxed); srtp_active_.store(stats.srtp_active, std::memory_order_relaxed);
rtt_ms_.store(stats.rtt_ms, std::memory_order_relaxed); rtt_ms_.store(stats.rtt_ms, std::memory_order_relaxed);
connection_latency_.Update(stats.rtt_ms);
} }
MiniRtcNetTrafficStats Load() const { MiniRtcNetTrafficStats Load() const {
@@ -55,7 +57,18 @@ class NetTrafficStatsCache {
return data_outbound_stats_.bitrate.load(std::memory_order_relaxed); return data_outbound_stats_.bitrate.load(std::memory_order_relaxed);
} }
void Reset() { Store(MiniRtcNetTrafficStats{}); } std::optional<double> SmoothedRttMs(
ConnectionLatencyStats::Clock::time_point now =
ConnectionLatencyStats::Clock::now()) const {
return connection_latency_.Get(now);
}
void ResetLatency() { connection_latency_.Reset(); }
void Reset() {
Store(MiniRtcNetTrafficStats{});
ResetLatency();
}
private: private:
struct InboundStats { struct InboundStats {
@@ -101,6 +114,7 @@ class NetTrafficStatsCache {
OutboundStats total_outbound_stats_; OutboundStats total_outbound_stats_;
std::atomic<bool> srtp_active_{false}; std::atomic<bool> srtp_active_{false};
std::atomic<double> rtt_ms_{-1}; std::atomic<double> rtt_ms_{-1};
ConnectionLatencyStats connection_latency_;
}; };
} // namespace crossdesk::gui_detail } // namespace crossdesk::gui_detail
@@ -511,6 +511,7 @@ void PeerEventHandler::OnNetStatusReport(
auto props = runtime->FindRemoteSession(remote_id); auto props = runtime->FindRemoteSession(remote_id);
if (!props) return; if (!props) return;
if (props->traversal_mode_ != mode) { if (props->traversal_mode_ != mode) {
props->net_traffic_stats_.ResetLatency();
props->traversal_mode_ = mode; props->traversal_mode_ = mode;
LOG_INFO("Net mode: [{}]", int(props->traversal_mode_)); LOG_INFO("Net mode: [{}]", int(props->traversal_mode_));
} }
@@ -3,11 +3,13 @@
#import <AppKit/AppKit.h> #import <AppKit/AppKit.h>
#import <CoreVideo/CVMetalTextureCache.h> #import <CoreVideo/CVMetalTextureCache.h>
#import <Metal/Metal.h> #import <Metal/Metal.h>
#import <QuartzCore/CABase.h>
#import <QuartzCore/CAMetalLayer.h> #import <QuartzCore/CAMetalLayer.h>
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <cmath>
#include <cstring> #include <cstring>
#include <limits> #include <limits>
#include <mutex> #include <mutex>
@@ -1265,9 +1267,21 @@ MacMetalVideoRenderer::RenderOutcome MacMetalVideoRenderer::RenderLatest(
slot.use = SlotUse::available; slot.use = SlotUse::available;
} }
}]; }];
[drawable addPresentedHandler:^(id<MTLDrawable> presented) {
// presentedTime uses the Core Animation host clock. Translate its age
// into our steady clock, excluding callback scheduling delay. Capturing
// timing by value keeps this safe after a session/renderer is destroyed.
const double presented_time = presented.presentedTime;
const auto now = VideoLatencyFrame::Clock::now();
const double age = CACurrentMediaTime() - presented_time;
if (presented_time > 0 && std::isfinite(age) && age >= 0 && age <= 5) {
timing.MarkPresented(
now - std::chrono::duration_cast<VideoLatencyFrame::Clock::duration>(
std::chrono::duration<double>(age)));
}
}];
[command_buffer presentDrawable:drawable]; [command_buffer presentDrawable:drawable];
[command_buffer commit]; [command_buffer commit];
timing.MarkSubmitted();
impl_->rendered_stream.assign(remote_id); impl_->rendered_stream.assign(remote_id);
impl_->rendered_content_generation = content_generation; impl_->rendered_content_generation = content_generation;
impl_->native_surface_needs_redraw->store(false, impl_->native_surface_needs_redraw->store(false,