Skip to content

Commit f08830b

Browse files
danielbotrosclaude
andcommitted
APP-14871: Report WebRTC connection metadata after dialing
Port of the Go SDK's WebRTC dial telemetry (goutils#583) for the Python/C++ SDKs. After a WebRTC dial finishes — success or failure — the client best-effort reports to the signaling server it dialed through: the furthest dial stage reached, the gRPC failure code, the dial duration, how the dial was signaled (cloud vs local), the selected ICE candidate pair per side (host/stun/relay + relay address), and the SDK type/version. Proto stubs generated from the goutils PR branch; regenerate with `make buf` once goutils#583 lands on the BSR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d9d1533 commit f08830b

6 files changed

Lines changed: 784 additions & 19 deletions

File tree

src/gen/proto.rpc.webrtc.v1.rs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,4 +375,210 @@ pub struct OptionalWebRtcConfigResponse {
375375
#[prost(message, optional, tag="1")]
376376
pub config: ::core::option::Option<WebRtcConfig>,
377377
}
378+
/// ConnectionCandidate describes the selected ICE candidate for one side of a WebRTC connection.
379+
#[allow(clippy::derive_partial_eq_without_eq)]
380+
#[derive(Clone, PartialEq, ::prost::Message)]
381+
pub struct ConnectionCandidate {
382+
#[prost(enumeration="IceCandidateType", tag="1")]
383+
pub r#type: i32,
384+
/// relay_address is the relay server address of this candidate; set only when type is
385+
/// RELAY, so the signaling server can classify the provider by matching against known
386+
/// coturn addresses.
387+
#[prost(string, tag="2")]
388+
pub relay_address: ::prost::alloc::string::String,
389+
}
390+
/// ReportConnectionMetadataRequest reports metadata about a WebRTC dial, per side: local is the
391+
/// dialing SDK and remote is the robot.
392+
#[allow(clippy::derive_partial_eq_without_eq)]
393+
#[derive(Clone, PartialEq, ::prost::Message)]
394+
pub struct ReportConnectionMetadataRequest {
395+
#[prost(message, optional, tag="1")]
396+
pub local: ::core::option::Option<ConnectionCandidate>,
397+
#[prost(message, optional, tag="2")]
398+
pub remote: ::core::option::Option<ConnectionCandidate>,
399+
#[prost(enumeration="SdkType", tag="3")]
400+
pub sdk_type: i32,
401+
/// reached_stage is the furthest dial checkpoint reached. READY indicates success; any earlier
402+
/// value is where a failed dial stopped.
403+
#[prost(enumeration="DialStage", tag="4")]
404+
pub reached_stage: i32,
405+
/// duration_ms is the wall-clock time from dial start to connection ready or to the failure.
406+
#[prost(uint32, tag="5")]
407+
pub duration_ms: u32,
408+
/// signaling_path is how the dial was signaled (cloud / local / mDNS); reported regardless of outcome.
409+
#[prost(enumeration="ConnectionSignalingPath", tag="6")]
410+
pub signaling_path: i32,
411+
/// failure_code is the gRPC status code of a failed dial
412+
#[prost(int32, tag="7")]
413+
pub failure_code: i32,
414+
/// sdk_version is the version of the dialing SDK (e.g. a semver or git tag).
415+
#[prost(string, tag="8")]
416+
pub sdk_version: ::prost::alloc::string::String,
417+
}
418+
/// ReportConnectionMetadataResponse is empty.
419+
#[allow(clippy::derive_partial_eq_without_eq)]
420+
#[derive(Clone, PartialEq, ::prost::Message)]
421+
pub struct ReportConnectionMetadataResponse {
422+
}
423+
/// ICECandidateType represents the type of ICE candidate selected for a WebRTC connection.
424+
/// The signaling server further classifies RELAY by relay server specific provider from the address.
425+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
426+
#[repr(i32)]
427+
pub enum IceCandidateType {
428+
Unspecified = 0,
429+
/// ICE_CANDIDATE_TYPE_HOST indicates a direct connection was established.
430+
Host = 1,
431+
/// ICE_CANDIDATE_TYPE_STUN indicates a STUN-assisted connection was established.
432+
Stun = 2,
433+
/// ICE_CANDIDATE_TYPE_RELAY indicates a TURN relay candidate was selected.
434+
Relay = 3,
435+
}
436+
impl IceCandidateType {
437+
/// String value of the enum field names used in the ProtoBuf definition.
438+
///
439+
/// The values are not transformed in any way and thus are considered stable
440+
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
441+
pub fn as_str_name(&self) -> &'static str {
442+
match self {
443+
IceCandidateType::Unspecified => "ICE_CANDIDATE_TYPE_UNSPECIFIED",
444+
IceCandidateType::Host => "ICE_CANDIDATE_TYPE_HOST",
445+
IceCandidateType::Stun => "ICE_CANDIDATE_TYPE_STUN",
446+
IceCandidateType::Relay => "ICE_CANDIDATE_TYPE_RELAY",
447+
}
448+
}
449+
/// Creates an enum from field names used in the ProtoBuf definition.
450+
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
451+
match value {
452+
"ICE_CANDIDATE_TYPE_UNSPECIFIED" => Some(Self::Unspecified),
453+
"ICE_CANDIDATE_TYPE_HOST" => Some(Self::Host),
454+
"ICE_CANDIDATE_TYPE_STUN" => Some(Self::Stun),
455+
"ICE_CANDIDATE_TYPE_RELAY" => Some(Self::Relay),
456+
_ => None,
457+
}
458+
}
459+
}
460+
/// SDKType represents the Viam SDK used to establish the connection.
461+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
462+
#[repr(i32)]
463+
pub enum SdkType {
464+
Unspecified = 0,
465+
Go = 1,
466+
Typescript = 2,
467+
PythonCpp = 3,
468+
}
469+
impl SdkType {
470+
/// String value of the enum field names used in the ProtoBuf definition.
471+
///
472+
/// The values are not transformed in any way and thus are considered stable
473+
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
474+
pub fn as_str_name(&self) -> &'static str {
475+
match self {
476+
SdkType::Unspecified => "SDK_TYPE_UNSPECIFIED",
477+
SdkType::Go => "SDK_TYPE_GO",
478+
SdkType::Typescript => "SDK_TYPE_TYPESCRIPT",
479+
SdkType::PythonCpp => "SDK_TYPE_PYTHON_CPP",
480+
}
481+
}
482+
/// Creates an enum from field names used in the ProtoBuf definition.
483+
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
484+
match value {
485+
"SDK_TYPE_UNSPECIFIED" => Some(Self::Unspecified),
486+
"SDK_TYPE_GO" => Some(Self::Go),
487+
"SDK_TYPE_TYPESCRIPT" => Some(Self::Typescript),
488+
"SDK_TYPE_PYTHON_CPP" => Some(Self::PythonCpp),
489+
_ => None,
490+
}
491+
}
492+
}
493+
/// DialStage is the furthest checkpoint a WebRTC dial reached. READY means the dial succeeded; any
494+
/// earlier value is the stage at which a failed dial stopped.
495+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
496+
#[repr(i32)]
497+
pub enum DialStage {
498+
Unspecified = 0,
499+
/// DIAL_STAGE_SIGNALING_CONNECTED: the signaling channel was established.
500+
SignalingConnected = 1,
501+
/// DIAL_STAGE_CONFIG_FETCHED: ICE/TURN configuration was fetched from the signaling server.
502+
ConfigFetched = 2,
503+
/// DIAL_STAGE_OFFER_SENT: the SDP offer was sent to the signaling server (the Call was accepted).
504+
OfferSent = 3,
505+
/// DIAL_STAGE_ANSWER_RECEIVED: the answerer's SDP answer was received and applied.
506+
AnswerReceived = 4,
507+
/// DIAL_STAGE_ICE_CONNECTED: ICE connectivity was established (a candidate pair connected).
508+
IceConnected = 5,
509+
/// DIAL_STAGE_DTLS_CONNECTED: the DTLS handshake completed (peer connection connected) but the
510+
/// data channel is not yet open.
511+
DtlsConnected = 6,
512+
/// DIAL_STAGE_READY: the connection is fully ready (data channel open). This is success.
513+
Ready = 7,
514+
}
515+
impl DialStage {
516+
/// String value of the enum field names used in the ProtoBuf definition.
517+
///
518+
/// The values are not transformed in any way and thus are considered stable
519+
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
520+
pub fn as_str_name(&self) -> &'static str {
521+
match self {
522+
DialStage::Unspecified => "DIAL_STAGE_UNSPECIFIED",
523+
DialStage::SignalingConnected => "DIAL_STAGE_SIGNALING_CONNECTED",
524+
DialStage::ConfigFetched => "DIAL_STAGE_CONFIG_FETCHED",
525+
DialStage::OfferSent => "DIAL_STAGE_OFFER_SENT",
526+
DialStage::AnswerReceived => "DIAL_STAGE_ANSWER_RECEIVED",
527+
DialStage::IceConnected => "DIAL_STAGE_ICE_CONNECTED",
528+
DialStage::DtlsConnected => "DIAL_STAGE_DTLS_CONNECTED",
529+
DialStage::Ready => "DIAL_STAGE_READY",
530+
}
531+
}
532+
/// Creates an enum from field names used in the ProtoBuf definition.
533+
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
534+
match value {
535+
"DIAL_STAGE_UNSPECIFIED" => Some(Self::Unspecified),
536+
"DIAL_STAGE_SIGNALING_CONNECTED" => Some(Self::SignalingConnected),
537+
"DIAL_STAGE_CONFIG_FETCHED" => Some(Self::ConfigFetched),
538+
"DIAL_STAGE_OFFER_SENT" => Some(Self::OfferSent),
539+
"DIAL_STAGE_ANSWER_RECEIVED" => Some(Self::AnswerReceived),
540+
"DIAL_STAGE_ICE_CONNECTED" => Some(Self::IceConnected),
541+
"DIAL_STAGE_DTLS_CONNECTED" => Some(Self::DtlsConnected),
542+
"DIAL_STAGE_READY" => Some(Self::Ready),
543+
_ => None,
544+
}
545+
}
546+
}
547+
/// ConnectionSignalingPath is how a WebRTC dial was signaled, derived from the signaling address.
548+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
549+
#[repr(i32)]
550+
pub enum ConnectionSignalingPath {
551+
Unspecified = 0,
552+
/// CONNECTION_SIGNALING_PATH_CLOUD_SIGNALED: signaled through app's signaling server.
553+
CloudSignaled = 1,
554+
/// CONNECTION_SIGNALING_PATH_MDNS_LOCAL: signaled over an mDNS-discovered local-network path.
555+
MdnsLocal = 2,
556+
/// CONNECTION_SIGNALING_PATH_LOCAL: signaled through a loopback/private-address signaling server
557+
/// (e.g. a machine's own signaling server) without mDNS discovery.
558+
Local = 3,
559+
}
560+
impl ConnectionSignalingPath {
561+
/// String value of the enum field names used in the ProtoBuf definition.
562+
///
563+
/// The values are not transformed in any way and thus are considered stable
564+
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
565+
pub fn as_str_name(&self) -> &'static str {
566+
match self {
567+
ConnectionSignalingPath::Unspecified => "CONNECTION_SIGNALING_PATH_UNSPECIFIED",
568+
ConnectionSignalingPath::CloudSignaled => "CONNECTION_SIGNALING_PATH_CLOUD_SIGNALED",
569+
ConnectionSignalingPath::MdnsLocal => "CONNECTION_SIGNALING_PATH_MDNS_LOCAL",
570+
ConnectionSignalingPath::Local => "CONNECTION_SIGNALING_PATH_LOCAL",
571+
}
572+
}
573+
/// Creates an enum from field names used in the ProtoBuf definition.
574+
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
575+
match value {
576+
"CONNECTION_SIGNALING_PATH_UNSPECIFIED" => Some(Self::Unspecified),
577+
"CONNECTION_SIGNALING_PATH_CLOUD_SIGNALED" => Some(Self::CloudSignaled),
578+
"CONNECTION_SIGNALING_PATH_MDNS_LOCAL" => Some(Self::MdnsLocal),
579+
"CONNECTION_SIGNALING_PATH_LOCAL" => Some(Self::Local),
580+
_ => None,
581+
}
582+
}
583+
}
378584
// @@protoc_insertion_point(module)

src/gen/proto.rpc.webrtc.v1.tonic.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,36 @@ pub mod signaling_service_client {
193193
);
194194
self.inner.unary(req, path, codec).await
195195
}
196+
pub async fn report_connection_metadata(
197+
&mut self,
198+
request: impl tonic::IntoRequest<super::ReportConnectionMetadataRequest>,
199+
) -> std::result::Result<
200+
tonic::Response<super::ReportConnectionMetadataResponse>,
201+
tonic::Status,
202+
> {
203+
self.inner
204+
.ready()
205+
.await
206+
.map_err(|e| {
207+
tonic::Status::new(
208+
tonic::Code::Unknown,
209+
format!("Service was not ready: {}", e.into()),
210+
)
211+
})?;
212+
let codec = tonic::codec::ProstCodec::default();
213+
let path = http::uri::PathAndQuery::from_static(
214+
"/proto.rpc.webrtc.v1.SignalingService/ReportConnectionMetadata",
215+
);
216+
let mut req = request.into_request();
217+
req.extensions_mut()
218+
.insert(
219+
GrpcMethod::new(
220+
"proto.rpc.webrtc.v1.SignalingService",
221+
"ReportConnectionMetadata",
222+
),
223+
);
224+
self.inner.unary(req, path, codec).await
225+
}
196226
}
197227
}
198228
/// Generated server implementations.
@@ -236,6 +266,13 @@ pub mod signaling_service_server {
236266
tonic::Response<super::OptionalWebRtcConfigResponse>,
237267
tonic::Status,
238268
>;
269+
async fn report_connection_metadata(
270+
&self,
271+
request: tonic::Request<super::ReportConnectionMetadataRequest>,
272+
) -> std::result::Result<
273+
tonic::Response<super::ReportConnectionMetadataResponse>,
274+
tonic::Status,
275+
>;
239276
}
240277
#[derive(Debug)]
241278
pub struct SignalingServiceServer<T: SignalingService> {
@@ -498,6 +535,54 @@ pub mod signaling_service_server {
498535
};
499536
Box::pin(fut)
500537
}
538+
"/proto.rpc.webrtc.v1.SignalingService/ReportConnectionMetadata" => {
539+
#[allow(non_camel_case_types)]
540+
struct ReportConnectionMetadataSvc<T: SignalingService>(pub Arc<T>);
541+
impl<
542+
T: SignalingService,
543+
> tonic::server::UnaryService<super::ReportConnectionMetadataRequest>
544+
for ReportConnectionMetadataSvc<T> {
545+
type Response = super::ReportConnectionMetadataResponse;
546+
type Future = BoxFuture<
547+
tonic::Response<Self::Response>,
548+
tonic::Status,
549+
>;
550+
fn call(
551+
&mut self,
552+
request: tonic::Request<
553+
super::ReportConnectionMetadataRequest,
554+
>,
555+
) -> Self::Future {
556+
let inner = Arc::clone(&self.0);
557+
let fut = async move {
558+
(*inner).report_connection_metadata(request).await
559+
};
560+
Box::pin(fut)
561+
}
562+
}
563+
let accept_compression_encodings = self.accept_compression_encodings;
564+
let send_compression_encodings = self.send_compression_encodings;
565+
let max_decoding_message_size = self.max_decoding_message_size;
566+
let max_encoding_message_size = self.max_encoding_message_size;
567+
let inner = self.inner.clone();
568+
let fut = async move {
569+
let inner = inner.0;
570+
let method = ReportConnectionMetadataSvc(inner);
571+
let codec = tonic::codec::ProstCodec::default();
572+
let mut grpc = tonic::server::Grpc::new(codec)
573+
.apply_compression_config(
574+
accept_compression_encodings,
575+
send_compression_encodings,
576+
)
577+
.apply_max_message_size_config(
578+
max_decoding_message_size,
579+
max_encoding_message_size,
580+
);
581+
let res = grpc.unary(method, req).await;
582+
Ok(res)
583+
};
584+
Box::pin(fut)
585+
}
501586
_ => {
502587
Box::pin(async move {
503588
Ok(

0 commit comments

Comments
 (0)