From cc55321980801591b6aba96c0bf0db64f3ea72cc Mon Sep 17 00:00:00 2001 From: danielbotros Date: Tue, 31 Mar 2026 11:28:19 -0400 Subject: [PATCH 1/2] APP-14871: Report connection metadata after WebRTC dial Co-Authored-By: Claude Sonnet 4.6 --- src/rpc/dial.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/src/rpc/dial.rs b/src/rpc/dial.rs index 4508943..2bb8e40 100644 --- a/src/rpc/dial.rs +++ b/src/rpc/dial.rs @@ -10,7 +10,8 @@ use crate::gen::proto::rpc::v1::{ use crate::gen::proto::rpc::webrtc::v1::{ call_response::Stage, call_update_request::Update, signaling_service_client::SignalingServiceClient, CallUpdateRequest, - OptionalWebRtcConfigRequest, OptionalWebRtcConfigResponse, + IceCandidateType, OptionalWebRtcConfigRequest, OptionalWebRtcConfigResponse, + ReportConnectionMetadataRequest, SdkType, }; use crate::gen::proto::rpc::webrtc::v1::{ CallRequest, IceCandidate, Metadata, RequestHeaders, Strings, @@ -1260,6 +1261,16 @@ async fn maybe_connect_via_webrtc( exchange_done.store(true, Ordering::Release); let uuid = uuid_lock.read().unwrap().to_string(); send_done_once(sent_done_or_error, &uuid, channel.clone()).await; + + // Best-effort report of connection metadata for per-org metrics. + { + let peer_conn = peer_connection.clone(); + let mut sc = SignalingServiceClient::new(channel.clone()); + tokio::spawn(async move { + report_connection_metadata(&mut sc, peer_conn).await; + }); + } + Ok(client_channel) } @@ -1340,3 +1351,83 @@ fn metadata_from_parts(parts: &http::request::Parts) -> Metadata { } Metadata { md } } + +/// Inspects a WebRTC stats report to determine which ICE candidate type was +/// selected for the connection. Mirrors the Go implementation in wrtc_client.go. +fn selected_ice_candidate_type( + stats: &::webrtc::stats::StatsReport, +) -> IceCandidateType { + use ::webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType; + use ::webrtc::stats::StatsReportType; + + // Find the nominated candidate pair and get its remote candidate ID. + let remote_cand_id = stats.reports.values().find_map(|s| { + if let StatsReportType::CandidatePair(p) = s { + if p.nominated { + return Some(p.remote_candidate_id.clone()); + } + } + None + }); + + let remote_cand_id = match remote_cand_id { + Some(id) if !id.is_empty() => id, + _ => return IceCandidateType::Unspecified, + }; + + let remote = stats.reports.get(&remote_cand_id).and_then(|s| { + if let StatsReportType::RemoteIceCandidate(c) = s { + Some(c) + } else { + None + } + }); + + match remote { + None => IceCandidateType::Unspecified, + Some(cand) => match cand.candidate_type { + RTCIceCandidateType::Host => IceCandidateType::Host, + RTCIceCandidateType::Srflx | RTCIceCandidateType::Prflx => IceCandidateType::Stun, + RTCIceCandidateType::Relay => { + if cand.url.is_empty() { + return IceCandidateType::Unspecified; + } + if cand.url.contains("viam.com") || cand.url.contains("viaminternal") { + IceCandidateType::CoturnRelay + } else { + IceCandidateType::TwilioRelay + } + } + _ => IceCandidateType::Unspecified, + }, + } +} + +/// Reports WebRTC connection metadata to the signaling server as a best-effort +/// fire-and-forget operation. Mirrors the Go implementation in wrtc_client.go. +async fn report_connection_metadata( + signaling_client: &mut SignalingServiceClient, + peer_conn: std::sync::Arc<::webrtc::peer_connection::RTCPeerConnection>, +) where + T: tonic::client::GrpcService, + T::Error: Into>, + T::ResponseBody: http_body::Body + Send + 'static, + ::Error: Into> + Send, +{ + let stats = peer_conn.get_stats().await; + let candidate_type = selected_ice_candidate_type(&stats); + if candidate_type == IceCandidateType::Unspecified { + log::debug!("could not determine selected ICE candidate type, skipping report"); + return; + } + + let request = tonic::Request::new(ReportConnectionMetadataRequest { + candidate_type: candidate_type as i32, + // TODO(APP-14871): use SDK_TYPE_RUST once the proto enum value is defined. + sdk_type: SdkType::Go as i32, + }); + + if let Err(e) = signaling_client.report_connection_metadata(request).await { + log::debug!("failed to report connection metadata: {e}"); + } +} From f6457b87321445cc795254c0af99221ad7a22ea6 Mon Sep 17 00:00:00 2001 From: danielbotros Date: Wed, 1 Apr 2026 09:55:12 -0400 Subject: [PATCH 2/2] Comments, use concrete signaling server type, use pythoncpp sdk type enum --- src/rpc/dial.rs | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/rpc/dial.rs b/src/rpc/dial.rs index 2bb8e40..28d4956 100644 --- a/src/rpc/dial.rs +++ b/src/rpc/dial.rs @@ -25,8 +25,10 @@ use ::http::{ use ::viam_mdns::{discover, Response}; use ::webrtc::ice_transport::{ ice_candidate::{RTCIceCandidate, RTCIceCandidateInit}, + ice_candidate_type::RTCIceCandidateType, ice_connection_state::RTCIceConnectionState, }; +use ::webrtc::stats::StatsReportType; use ::webrtc::peer_connection::sdp::session_description::RTCSessionDescription; use anyhow::{Context, Result}; use core::fmt; @@ -331,7 +333,7 @@ impl DialBuilder { for ipv4 in addresses { for candidate in candidates { let discovery = discover::interface_with_loopback( - VIAM_MDNS_SERVICE_NAME, + VIAM_MDNS_SERVICE_NAME,f Duration::from_millis(250), ipv4, ) @@ -1262,7 +1264,6 @@ async fn maybe_connect_via_webrtc( let uuid = uuid_lock.read().unwrap().to_string(); send_done_once(sent_done_or_error, &uuid, channel.clone()).await; - // Best-effort report of connection metadata for per-org metrics. { let peer_conn = peer_connection.clone(); let mut sc = SignalingServiceClient::new(channel.clone()); @@ -1353,13 +1354,10 @@ fn metadata_from_parts(parts: &http::request::Parts) -> Metadata { } /// Inspects a WebRTC stats report to determine which ICE candidate type was -/// selected for the connection. Mirrors the Go implementation in wrtc_client.go. +/// selected for the connection. fn selected_ice_candidate_type( stats: &::webrtc::stats::StatsReport, ) -> IceCandidateType { - use ::webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType; - use ::webrtc::stats::StatsReportType; - // Find the nominated candidate pair and get its remote candidate ID. let remote_cand_id = stats.reports.values().find_map(|s| { if let StatsReportType::CandidatePair(p) = s { @@ -1392,7 +1390,7 @@ fn selected_ice_candidate_type( if cand.url.is_empty() { return IceCandidateType::Unspecified; } - if cand.url.contains("viam.com") || cand.url.contains("viaminternal") { + if cand.url.contains("viam.com") { IceCandidateType::CoturnRelay } else { IceCandidateType::TwilioRelay @@ -1404,16 +1402,11 @@ fn selected_ice_candidate_type( } /// Reports WebRTC connection metadata to the signaling server as a best-effort -/// fire-and-forget operation. Mirrors the Go implementation in wrtc_client.go. -async fn report_connection_metadata( - signaling_client: &mut SignalingServiceClient, +/// fire-and-forget operation. +async fn report_connection_metadata( + signaling_client: &mut SignalingServiceClient>>, peer_conn: std::sync::Arc<::webrtc::peer_connection::RTCPeerConnection>, -) where - T: tonic::client::GrpcService, - T::Error: Into>, - T::ResponseBody: http_body::Body + Send + 'static, - ::Error: Into> + Send, -{ +) { let stats = peer_conn.get_stats().await; let candidate_type = selected_ice_candidate_type(&stats); if candidate_type == IceCandidateType::Unspecified { @@ -1423,8 +1416,7 @@ async fn report_connection_metadata( let request = tonic::Request::new(ReportConnectionMetadataRequest { candidate_type: candidate_type as i32, - // TODO(APP-14871): use SDK_TYPE_RUST once the proto enum value is defined. - sdk_type: SdkType::Go as i32, + sdk_type: SdkType::PythonCpp as i32, }); if let Err(e) = signaling_client.report_connection_metadata(request).await {