From ff47c71963976d9d73969ea7510d6f5c75cb2907 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:32:21 -0700 Subject: [PATCH 01/28] Pin protocol to ladvoc/schema-metadata --- livekit-protocol/protocol | 2 +- livekit-protocol/src/livekit.rs | 218 ++++- livekit-protocol/src/livekit.serde.rs | 1168 +++++++++++++++++++++++-- 3 files changed, 1290 insertions(+), 98 deletions(-) diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index 4b09446be..dfa6d45c5 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit 4b09446beca5b3b5b02a3c424655f386521ca008 +Subproject commit dfa6d45c5cc4e457e8a58a7022f9ac8098cb63c6 diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index 975c95629..e36d4539b 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -632,6 +632,25 @@ pub struct DataTrackInfo { /// Method used for end-to-end encryption (E2EE) on packet payloads. #[prost(enumeration="encryption::Type", tag="4")] pub encryption: i32, + /// Encoding for frame payloads on this track. If unspecified, the track is untyped. + #[prost(enumeration="DataTrackFrameEncoding", optional, tag="5")] + pub frame_encoding: ::core::option::Option, + /// ID of the schema used by frames on this track if the track is typed. + #[prost(message, optional, tag="6")] + pub schema: ::core::option::Option, +} +/// Identifier for a data track schema. +/// +/// Schemas with the same name but different encodings are distinct. +/// +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackSchemaId { + /// This must be non-empty and no longer than 256 characters. + #[prost(string, tag="1")] + pub name: ::prost::alloc::string::String, + #[prost(enumeration="DataTrackSchemaEncoding", tag="2")] + pub encoding: i32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -649,6 +668,37 @@ pub struct DataTrackSubscriptionOptions { #[prost(uint32, optional, tag="1")] pub target_fps: ::core::option::Option, } +/// Key used to uniquely identify a data blob for storage and retrieval. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataBlobKey { + #[prost(oneof="data_blob_key::Key", tags="1, 2")] + pub key: ::core::option::Option, +} +/// Nested message and enum types in `DataBlobKey`. +pub mod data_blob_key { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Key { + /// Generic string key, blob contains arbitrary data. + #[prost(string, tag="1")] + Generic(::prost::alloc::string::String), + /// Data track schema identifier, blob contains schema definition. + #[prost(message, tag="2")] + SchemaId(super::DataTrackSchemaId), + } +} +/// A blob of data stored in a room identified by a unique key. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataBlob { + /// Unique key the data blob is identified by. + #[prost(message, optional, tag="1")] + pub key: ::core::option::Option, + /// Contents of the data blob. This must not exceed 50 KB. + #[prost(bytes="vec", tag="2")] + pub contents: ::prost::alloc::vec::Vec, +} /// provide information about available spatial layers #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -1780,6 +1830,118 @@ impl TrackSource { } } } +/// Encoding for frame payloads. +/// +/// Mirrors the well-known message encodings from the MCAP spec: +/// +/// +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum DataTrackFrameEncoding { + Unspecified = 0, + /// ROS 1: must be described by `ROS1_MSG` schema encoding. + Ros1 = 1, + /// CDR: must be described by `ROS2_MSG`, `ROS2_IDL`, or `OMG_IDL` schema encoding. + Cdr = 2, + /// Protocol Buffer: must be described by `PROTOBUF` schema encoding. + Protobuf = 3, + /// FlatBuffer: must be described by `FLATBUFFER` schema encoding. + Flatbuffer = 4, + /// CBOR: self-describing. + Cbor = 5, + /// MessagePack: self-describing. + Msgpack = 6, + /// JSON: self-describing or described by `JSON_SCHEMA` schema encoding. + Json = 7, +} +impl DataTrackFrameEncoding { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + DataTrackFrameEncoding::Unspecified => "DATA_TRACK_FRAME_ENCODING_UNSPECIFIED", + DataTrackFrameEncoding::Ros1 => "DATA_TRACK_FRAME_ENCODING_ROS1", + DataTrackFrameEncoding::Cdr => "DATA_TRACK_FRAME_ENCODING_CDR", + DataTrackFrameEncoding::Protobuf => "DATA_TRACK_FRAME_ENCODING_PROTOBUF", + DataTrackFrameEncoding::Flatbuffer => "DATA_TRACK_FRAME_ENCODING_FLATBUFFER", + DataTrackFrameEncoding::Cbor => "DATA_TRACK_FRAME_ENCODING_CBOR", + DataTrackFrameEncoding::Msgpack => "DATA_TRACK_FRAME_ENCODING_MSGPACK", + DataTrackFrameEncoding::Json => "DATA_TRACK_FRAME_ENCODING_JSON", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "DATA_TRACK_FRAME_ENCODING_UNSPECIFIED" => Some(Self::Unspecified), + "DATA_TRACK_FRAME_ENCODING_ROS1" => Some(Self::Ros1), + "DATA_TRACK_FRAME_ENCODING_CDR" => Some(Self::Cdr), + "DATA_TRACK_FRAME_ENCODING_PROTOBUF" => Some(Self::Protobuf), + "DATA_TRACK_FRAME_ENCODING_FLATBUFFER" => Some(Self::Flatbuffer), + "DATA_TRACK_FRAME_ENCODING_CBOR" => Some(Self::Cbor), + "DATA_TRACK_FRAME_ENCODING_MSGPACK" => Some(Self::Msgpack), + "DATA_TRACK_FRAME_ENCODING_JSON" => Some(Self::Json), + _ => None, + } + } +} +/// Encoding for schema definitions. +/// +/// Mirrors the well-known schema encodings from the MCAP spec: +/// +/// +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum DataTrackSchemaEncoding { + Unspecified = 0, + /// Protocol Buffer IDL: describes `PROTOBUF` frame encoding. + Protobuf = 1, + /// FlatBuffer IDL: describes `FLATBUFFER` frame encoding. + Flatbuffer = 2, + /// ROS 1 Message: describes `ROS1` frame encoding. + Ros1Msg = 3, + /// ROS 2 Message: describes `CDR` frame encoding. + Ros2Msg = 4, + /// ROS 2 IDL: describes `CDR` frame encoding. + Ros2Idl = 5, + /// OMG IDL: describes `CDR` frame encoding. + OmgIdl = 6, + /// JSON Schema: describes `JSON` frame encoding. + JsonSchema = 7, +} +impl DataTrackSchemaEncoding { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + DataTrackSchemaEncoding::Unspecified => "DATA_TRACK_SCHEMA_ENCODING_UNSPECIFIED", + DataTrackSchemaEncoding::Protobuf => "DATA_TRACK_SCHEMA_ENCODING_PROTOBUF", + DataTrackSchemaEncoding::Flatbuffer => "DATA_TRACK_SCHEMA_ENCODING_FLATBUFFER", + DataTrackSchemaEncoding::Ros1Msg => "DATA_TRACK_SCHEMA_ENCODING_ROS1_MSG", + DataTrackSchemaEncoding::Ros2Msg => "DATA_TRACK_SCHEMA_ENCODING_ROS2_MSG", + DataTrackSchemaEncoding::Ros2Idl => "DATA_TRACK_SCHEMA_ENCODING_ROS2_IDL", + DataTrackSchemaEncoding::OmgIdl => "DATA_TRACK_SCHEMA_ENCODING_OMG_IDL", + DataTrackSchemaEncoding::JsonSchema => "DATA_TRACK_SCHEMA_ENCODING_JSON_SCHEMA", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "DATA_TRACK_SCHEMA_ENCODING_UNSPECIFIED" => Some(Self::Unspecified), + "DATA_TRACK_SCHEMA_ENCODING_PROTOBUF" => Some(Self::Protobuf), + "DATA_TRACK_SCHEMA_ENCODING_FLATBUFFER" => Some(Self::Flatbuffer), + "DATA_TRACK_SCHEMA_ENCODING_ROS1_MSG" => Some(Self::Ros1Msg), + "DATA_TRACK_SCHEMA_ENCODING_ROS2_MSG" => Some(Self::Ros2Msg), + "DATA_TRACK_SCHEMA_ENCODING_ROS2_IDL" => Some(Self::Ros2Idl), + "DATA_TRACK_SCHEMA_ENCODING_OMG_IDL" => Some(Self::OmgIdl), + "DATA_TRACK_SCHEMA_ENCODING_JSON_SCHEMA" => Some(Self::JsonSchema), + _ => None, + } + } +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum DataTrackExtensionId { @@ -2652,7 +2814,7 @@ pub struct EgressInfo { pub backup_storage_used: bool, #[prost(int32, tag="27")] pub retry_count: i32, - #[prost(oneof="egress_info::Request", tags="30, 4, 14, 19, 5, 6")] + #[prost(oneof="egress_info::Request", tags="29, 30, 4, 14, 19, 5, 6")] pub request: ::core::option::Option, // next ID: 31 @@ -2665,7 +2827,8 @@ pub mod egress_info { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Request { - /// StartEgressRequest egress = 29; + #[prost(message, tag="29")] + Egress(super::StartEgressRequest), #[prost(message, tag="30")] Replay(super::ExportReplayRequest), #[prost(message, tag="4")] @@ -3497,7 +3660,7 @@ impl AudioMixing { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SignalRequest { - #[prost(oneof="signal_request::Message", tags="1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21")] + #[prost(oneof="signal_request::Message", tags="1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalRequest`. @@ -3565,12 +3728,18 @@ pub mod signal_request { /// Update subscription state for one or more data tracks #[prost(message, tag="21")] UpdateDataSubscription(super::UpdateDataSubscription), + /// Store a data blob. + #[prost(message, tag="22")] + StoreDataBlobRequest(super::StoreDataBlobRequest), + /// Retrieve a stored data blob. + #[prost(message, tag="23")] + GetDataBlobRequest(super::GetDataBlobRequest), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SignalResponse { - #[prost(oneof="signal_response::Message", tags="1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29")] + #[prost(oneof="signal_response::Message", tags="1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalResponse`. @@ -3665,6 +3834,9 @@ pub mod signal_response { /// Sent to data track subscribers to provide mapping from track SIDs to handles. #[prost(message, tag="29")] DataTrackSubscriberHandles(super::DataTrackSubscriberHandles), + /// Sent in response to `GetDataBlobRequest`. + #[prost(message, tag="30")] + GetDataBlobResponse(super::GetDataBlobResponse), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -3745,6 +3917,13 @@ pub struct PublishDataTrackRequest { /// Method used for end-to-end encryption (E2EE) on frame payloads. #[prost(enumeration="encryption::Type", tag="3")] pub encryption: i32, + /// Encoding for frame payloads on this track. If unspecified, the track is untyped. + #[prost(enumeration="DataTrackFrameEncoding", optional, tag="4")] + pub frame_encoding: ::core::option::Option, + /// ID of the schema used by frames on this track if the track is typed. + /// If set, the associated schema must be stored with `StoreDataBlobRequest`. + #[prost(message, optional, tag="5")] + pub schema: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3925,6 +4104,28 @@ pub mod update_data_subscription { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct StoreDataBlobRequest { + #[prost(message, optional, tag="1")] + pub blob: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetDataBlobRequest { + /// Identity of the participant who owns the blob. + #[prost(string, tag="1")] + pub participant_identity: ::prost::alloc::string::String, + /// Unique key of the data blob to retrieve. + #[prost(message, optional, tag="2")] + pub key: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetDataBlobResponse { + #[prost(message, optional, tag="1")] + pub blob: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct UpdateTrackSettings { #[prost(string, repeated, tag="1")] pub track_sids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, @@ -4314,7 +4515,7 @@ pub struct RequestResponse { pub reason: i32, #[prost(string, tag="3")] pub message: ::prost::alloc::string::String, - #[prost(oneof="request_response::Request", tags="4, 5, 6, 7, 8, 9, 10, 11")] + #[prost(oneof="request_response::Request", tags="4, 5, 6, 7, 8, 9, 10, 11, 12, 13")] pub request: ::core::option::Option, } /// Nested message and enum types in `RequestResponse`. @@ -4333,6 +4534,7 @@ pub mod request_response { InvalidName = 8, DuplicateHandle = 9, DuplicateName = 10, + InvalidRequest = 11, } impl Reason { /// String value of the enum field names used in the ProtoBuf definition. @@ -4352,6 +4554,7 @@ pub mod request_response { Reason::InvalidName => "INVALID_NAME", Reason::DuplicateHandle => "DUPLICATE_HANDLE", Reason::DuplicateName => "DUPLICATE_NAME", + Reason::InvalidRequest => "INVALID_REQUEST", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -4368,6 +4571,7 @@ pub mod request_response { "INVALID_NAME" => Some(Self::InvalidName), "DUPLICATE_HANDLE" => Some(Self::DuplicateHandle), "DUPLICATE_NAME" => Some(Self::DuplicateName), + "INVALID_REQUEST" => Some(Self::InvalidRequest), _ => None, } } @@ -4391,6 +4595,10 @@ pub mod request_response { PublishDataTrack(super::PublishDataTrackRequest), #[prost(message, tag="11")] UnpublishDataTrack(super::UnpublishDataTrackRequest), + #[prost(message, tag="12")] + StoreDataBlob(super::StoreDataBlobRequest), + #[prost(message, tag="13")] + GetDataBlob(super::GetDataBlobRequest), } } #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index c80ca9821..1456cf503 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -10468,6 +10468,235 @@ impl<'de> serde::Deserialize<'de> for CreateSipTrunkRequest { deserializer.deserialize_struct("livekit.CreateSIPTrunkRequest", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for DataBlob { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.key.is_some() { + len += 1; + } + if !self.contents.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataBlob", len)?; + if let Some(v) = self.key.as_ref() { + struct_ser.serialize_field("key", v)?; + } + if !self.contents.is_empty() { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("contents", pbjson::private::base64::encode(&self.contents).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataBlob { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "key", + "contents", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Key, + Contents, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "key" => Ok(GeneratedField::Key), + "contents" => Ok(GeneratedField::Contents), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataBlob; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataBlob") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut key__ = None; + let mut contents__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Key => { + if key__.is_some() { + return Err(serde::de::Error::duplicate_field("key")); + } + key__ = map_.next_value()?; + } + GeneratedField::Contents => { + if contents__.is_some() { + return Err(serde::de::Error::duplicate_field("contents")); + } + contents__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataBlob { + key: key__, + contents: contents__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DataBlob", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DataBlobKey { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.key.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataBlobKey", len)?; + if let Some(v) = self.key.as_ref() { + match v { + data_blob_key::Key::Generic(v) => { + struct_ser.serialize_field("generic", v)?; + } + data_blob_key::Key::SchemaId(v) => { + struct_ser.serialize_field("schemaId", v)?; + } + } + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataBlobKey { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "generic", + "schema_id", + "schemaId", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Generic, + SchemaId, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "generic" => Ok(GeneratedField::Generic), + "schemaId" | "schema_id" => Ok(GeneratedField::SchemaId), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataBlobKey; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataBlobKey") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut key__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Generic => { + if key__.is_some() { + return Err(serde::de::Error::duplicate_field("generic")); + } + key__ = map_.next_value::<::std::option::Option<_>>()?.map(data_blob_key::Key::Generic); + } + GeneratedField::SchemaId => { + if key__.is_some() { + return Err(serde::de::Error::duplicate_field("schemaId")); + } + key__ = map_.next_value::<::std::option::Option<_>>()?.map(data_blob_key::Key::SchemaId) +; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataBlobKey { + key: key__, + }) + } + } + deserializer.deserialize_struct("livekit.DataBlobKey", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for DataChannelInfo { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -12526,6 +12755,95 @@ impl<'de> serde::Deserialize<'de> for DataTrackExtensionParticipantSid { deserializer.deserialize_struct("livekit.DataTrackExtensionParticipantSid", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for DataTrackFrameEncoding { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Unspecified => "DATA_TRACK_FRAME_ENCODING_UNSPECIFIED", + Self::Ros1 => "DATA_TRACK_FRAME_ENCODING_ROS1", + Self::Cdr => "DATA_TRACK_FRAME_ENCODING_CDR", + Self::Protobuf => "DATA_TRACK_FRAME_ENCODING_PROTOBUF", + Self::Flatbuffer => "DATA_TRACK_FRAME_ENCODING_FLATBUFFER", + Self::Cbor => "DATA_TRACK_FRAME_ENCODING_CBOR", + Self::Msgpack => "DATA_TRACK_FRAME_ENCODING_MSGPACK", + Self::Json => "DATA_TRACK_FRAME_ENCODING_JSON", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for DataTrackFrameEncoding { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "DATA_TRACK_FRAME_ENCODING_UNSPECIFIED", + "DATA_TRACK_FRAME_ENCODING_ROS1", + "DATA_TRACK_FRAME_ENCODING_CDR", + "DATA_TRACK_FRAME_ENCODING_PROTOBUF", + "DATA_TRACK_FRAME_ENCODING_FLATBUFFER", + "DATA_TRACK_FRAME_ENCODING_CBOR", + "DATA_TRACK_FRAME_ENCODING_MSGPACK", + "DATA_TRACK_FRAME_ENCODING_JSON", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackFrameEncoding; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + fn visit_i64(self, v: i64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self) + }) + } + + fn visit_u64(self, v: u64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self) + }) + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "DATA_TRACK_FRAME_ENCODING_UNSPECIFIED" => Ok(DataTrackFrameEncoding::Unspecified), + "DATA_TRACK_FRAME_ENCODING_ROS1" => Ok(DataTrackFrameEncoding::Ros1), + "DATA_TRACK_FRAME_ENCODING_CDR" => Ok(DataTrackFrameEncoding::Cdr), + "DATA_TRACK_FRAME_ENCODING_PROTOBUF" => Ok(DataTrackFrameEncoding::Protobuf), + "DATA_TRACK_FRAME_ENCODING_FLATBUFFER" => Ok(DataTrackFrameEncoding::Flatbuffer), + "DATA_TRACK_FRAME_ENCODING_CBOR" => Ok(DataTrackFrameEncoding::Cbor), + "DATA_TRACK_FRAME_ENCODING_MSGPACK" => Ok(DataTrackFrameEncoding::Msgpack), + "DATA_TRACK_FRAME_ENCODING_JSON" => Ok(DataTrackFrameEncoding::Json), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} impl serde::Serialize for DataTrackInfo { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -12546,6 +12864,12 @@ impl serde::Serialize for DataTrackInfo { if self.encryption != 0 { len += 1; } + if self.frame_encoding.is_some() { + len += 1; + } + if self.schema.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.DataTrackInfo", len)?; if self.pub_handle != 0 { struct_ser.serialize_field("pubHandle", &self.pub_handle)?; @@ -12561,6 +12885,14 @@ impl serde::Serialize for DataTrackInfo { .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; struct_ser.serialize_field("encryption", &v)?; } + if let Some(v) = self.frame_encoding.as_ref() { + let v = DataTrackFrameEncoding::try_from(*v) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", *v)))?; + struct_ser.serialize_field("frameEncoding", &v)?; + } + if let Some(v) = self.schema.as_ref() { + struct_ser.serialize_field("schema", v)?; + } struct_ser.end() } } @@ -12576,6 +12908,9 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { "sid", "name", "encryption", + "frame_encoding", + "frameEncoding", + "schema", ]; #[allow(clippy::enum_variant_names)] @@ -12584,6 +12919,8 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { Sid, Name, Encryption, + FrameEncoding, + Schema, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -12610,6 +12947,8 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { "sid" => Ok(GeneratedField::Sid), "name" => Ok(GeneratedField::Name), "encryption" => Ok(GeneratedField::Encryption), + "frameEncoding" | "frame_encoding" => Ok(GeneratedField::FrameEncoding), + "schema" => Ok(GeneratedField::Schema), _ => Ok(GeneratedField::__SkipField__), } } @@ -12633,6 +12972,8 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { let mut sid__ = None; let mut name__ = None; let mut encryption__ = None; + let mut frame_encoding__ = None; + let mut schema__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::PubHandle => { @@ -12661,6 +13002,18 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { } encryption__ = Some(map_.next_value::()? as i32); } + GeneratedField::FrameEncoding => { + if frame_encoding__.is_some() { + return Err(serde::de::Error::duplicate_field("frameEncoding")); + } + frame_encoding__ = map_.next_value::<::std::option::Option>()?.map(|x| x as i32); + } + GeneratedField::Schema => { + if schema__.is_some() { + return Err(serde::de::Error::duplicate_field("schema")); + } + schema__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -12671,12 +13024,217 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { sid: sid__.unwrap_or_default(), name: name__.unwrap_or_default(), encryption: encryption__.unwrap_or_default(), + frame_encoding: frame_encoding__, + schema: schema__, }) } } deserializer.deserialize_struct("livekit.DataTrackInfo", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for DataTrackSchemaEncoding { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Unspecified => "DATA_TRACK_SCHEMA_ENCODING_UNSPECIFIED", + Self::Protobuf => "DATA_TRACK_SCHEMA_ENCODING_PROTOBUF", + Self::Flatbuffer => "DATA_TRACK_SCHEMA_ENCODING_FLATBUFFER", + Self::Ros1Msg => "DATA_TRACK_SCHEMA_ENCODING_ROS1_MSG", + Self::Ros2Msg => "DATA_TRACK_SCHEMA_ENCODING_ROS2_MSG", + Self::Ros2Idl => "DATA_TRACK_SCHEMA_ENCODING_ROS2_IDL", + Self::OmgIdl => "DATA_TRACK_SCHEMA_ENCODING_OMG_IDL", + Self::JsonSchema => "DATA_TRACK_SCHEMA_ENCODING_JSON_SCHEMA", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for DataTrackSchemaEncoding { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "DATA_TRACK_SCHEMA_ENCODING_UNSPECIFIED", + "DATA_TRACK_SCHEMA_ENCODING_PROTOBUF", + "DATA_TRACK_SCHEMA_ENCODING_FLATBUFFER", + "DATA_TRACK_SCHEMA_ENCODING_ROS1_MSG", + "DATA_TRACK_SCHEMA_ENCODING_ROS2_MSG", + "DATA_TRACK_SCHEMA_ENCODING_ROS2_IDL", + "DATA_TRACK_SCHEMA_ENCODING_OMG_IDL", + "DATA_TRACK_SCHEMA_ENCODING_JSON_SCHEMA", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackSchemaEncoding; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + fn visit_i64(self, v: i64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self) + }) + } + + fn visit_u64(self, v: u64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self) + }) + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "DATA_TRACK_SCHEMA_ENCODING_UNSPECIFIED" => Ok(DataTrackSchemaEncoding::Unspecified), + "DATA_TRACK_SCHEMA_ENCODING_PROTOBUF" => Ok(DataTrackSchemaEncoding::Protobuf), + "DATA_TRACK_SCHEMA_ENCODING_FLATBUFFER" => Ok(DataTrackSchemaEncoding::Flatbuffer), + "DATA_TRACK_SCHEMA_ENCODING_ROS1_MSG" => Ok(DataTrackSchemaEncoding::Ros1Msg), + "DATA_TRACK_SCHEMA_ENCODING_ROS2_MSG" => Ok(DataTrackSchemaEncoding::Ros2Msg), + "DATA_TRACK_SCHEMA_ENCODING_ROS2_IDL" => Ok(DataTrackSchemaEncoding::Ros2Idl), + "DATA_TRACK_SCHEMA_ENCODING_OMG_IDL" => Ok(DataTrackSchemaEncoding::OmgIdl), + "DATA_TRACK_SCHEMA_ENCODING_JSON_SCHEMA" => Ok(DataTrackSchemaEncoding::JsonSchema), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for DataTrackSchemaId { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.name.is_empty() { + len += 1; + } + if self.encoding != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSchemaId", len)?; + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + if self.encoding != 0 { + let v = DataTrackSchemaEncoding::try_from(self.encoding) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encoding)))?; + struct_ser.serialize_field("encoding", &v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataTrackSchemaId { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "name", + "encoding", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Name, + Encoding, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "name" => Ok(GeneratedField::Name), + "encoding" => Ok(GeneratedField::Encoding), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackSchemaId; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackSchemaId") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut name__ = None; + let mut encoding__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Name => { + if name__.is_some() { + return Err(serde::de::Error::duplicate_field("name")); + } + name__ = Some(map_.next_value()?); + } + GeneratedField::Encoding => { + if encoding__.is_some() { + return Err(serde::de::Error::duplicate_field("encoding")); + } + encoding__ = Some(map_.next_value::()? as i32); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataTrackSchemaId { + name: name__.unwrap_or_default(), + encoding: encoding__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackSchemaId", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for DataTrackSubscriberHandles { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -14944,6 +15502,9 @@ impl serde::Serialize for EgressInfo { } if let Some(v) = self.request.as_ref() { match v { + egress_info::Request::Egress(v) => { + struct_ser.serialize_field("egress", v)?; + } egress_info::Request::Replay(v) => { struct_ser.serialize_field("replay", v)?; } @@ -15020,6 +15581,7 @@ impl<'de> serde::Deserialize<'de> for EgressInfo { "backupStorageUsed", "retry_count", "retryCount", + "egress", "replay", "room_composite", "roomComposite", @@ -15053,6 +15615,7 @@ impl<'de> serde::Deserialize<'de> for EgressInfo { ManifestLocation, BackupStorageUsed, RetryCount, + Egress, Replay, RoomComposite, Web, @@ -15102,6 +15665,7 @@ impl<'de> serde::Deserialize<'de> for EgressInfo { "manifestLocation" | "manifest_location" => Ok(GeneratedField::ManifestLocation), "backupStorageUsed" | "backup_storage_used" => Ok(GeneratedField::BackupStorageUsed), "retryCount" | "retry_count" => Ok(GeneratedField::RetryCount), + "egress" => Ok(GeneratedField::Egress), "replay" => Ok(GeneratedField::Replay), "roomComposite" | "room_composite" => Ok(GeneratedField::RoomComposite), "web" => Ok(GeneratedField::Web), @@ -15270,6 +15834,13 @@ impl<'de> serde::Deserialize<'de> for EgressInfo { Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) ; } + GeneratedField::Egress => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("egress")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(egress_info::Request::Egress) +; + } GeneratedField::Replay => { if request__.is_some() { return Err(serde::de::Error::duplicate_field("replay")); @@ -18064,9 +18635,210 @@ impl<'de> serde::Deserialize<'de> for ForwardParticipantRequest { E: serde::de::Error, { match value { - "room" => Ok(GeneratedField::Room), - "identity" => Ok(GeneratedField::Identity), - "destinationRoom" | "destination_room" => Ok(GeneratedField::DestinationRoom), + "room" => Ok(GeneratedField::Room), + "identity" => Ok(GeneratedField::Identity), + "destinationRoom" | "destination_room" => Ok(GeneratedField::DestinationRoom), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ForwardParticipantRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.ForwardParticipantRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut room__ = None; + let mut identity__ = None; + let mut destination_room__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Room => { + if room__.is_some() { + return Err(serde::de::Error::duplicate_field("room")); + } + room__ = Some(map_.next_value()?); + } + GeneratedField::Identity => { + if identity__.is_some() { + return Err(serde::de::Error::duplicate_field("identity")); + } + identity__ = Some(map_.next_value()?); + } + GeneratedField::DestinationRoom => { + if destination_room__.is_some() { + return Err(serde::de::Error::duplicate_field("destinationRoom")); + } + destination_room__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(ForwardParticipantRequest { + room: room__.unwrap_or_default(), + identity: identity__.unwrap_or_default(), + destination_room: destination_room__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.ForwardParticipantRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ForwardParticipantResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("livekit.ForwardParticipantResponse", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ForwardParticipantResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Ok(GeneratedField::__SkipField__) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ForwardParticipantResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.ForwardParticipantResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(ForwardParticipantResponse { + }) + } + } + deserializer.deserialize_struct("livekit.ForwardParticipantResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GcpUpload { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.credentials.is_empty() { + len += 1; + } + if !self.bucket.is_empty() { + len += 1; + } + if self.proxy.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.GCPUpload", len)?; + if !self.credentials.is_empty() { + struct_ser.serialize_field("credentials", &self.credentials)?; + } + if !self.bucket.is_empty() { + struct_ser.serialize_field("bucket", &self.bucket)?; + } + if let Some(v) = self.proxy.as_ref() { + struct_ser.serialize_field("proxy", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GcpUpload { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "credentials", + "bucket", + "proxy", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Credentials, + Bucket, + Proxy, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "credentials" => Ok(GeneratedField::Credentials), + "bucket" => Ok(GeneratedField::Bucket), + "proxy" => Ok(GeneratedField::Proxy), _ => Ok(GeneratedField::__SkipField__), } } @@ -18076,77 +18848,94 @@ impl<'de> serde::Deserialize<'de> for ForwardParticipantRequest { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = ForwardParticipantRequest; + type Value = GcpUpload; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.ForwardParticipantRequest") + formatter.write_str("struct livekit.GCPUpload") } - fn visit_map(self, mut map_: V) -> std::result::Result + fn visit_map(self, mut map_: V) -> std::result::Result where V: serde::de::MapAccess<'de>, { - let mut room__ = None; - let mut identity__ = None; - let mut destination_room__ = None; + let mut credentials__ = None; + let mut bucket__ = None; + let mut proxy__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Room => { - if room__.is_some() { - return Err(serde::de::Error::duplicate_field("room")); + GeneratedField::Credentials => { + if credentials__.is_some() { + return Err(serde::de::Error::duplicate_field("credentials")); } - room__ = Some(map_.next_value()?); + credentials__ = Some(map_.next_value()?); } - GeneratedField::Identity => { - if identity__.is_some() { - return Err(serde::de::Error::duplicate_field("identity")); + GeneratedField::Bucket => { + if bucket__.is_some() { + return Err(serde::de::Error::duplicate_field("bucket")); } - identity__ = Some(map_.next_value()?); + bucket__ = Some(map_.next_value()?); } - GeneratedField::DestinationRoom => { - if destination_room__.is_some() { - return Err(serde::de::Error::duplicate_field("destinationRoom")); + GeneratedField::Proxy => { + if proxy__.is_some() { + return Err(serde::de::Error::duplicate_field("proxy")); } - destination_room__ = Some(map_.next_value()?); + proxy__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(ForwardParticipantRequest { - room: room__.unwrap_or_default(), - identity: identity__.unwrap_or_default(), - destination_room: destination_room__.unwrap_or_default(), + Ok(GcpUpload { + credentials: credentials__.unwrap_or_default(), + bucket: bucket__.unwrap_or_default(), + proxy: proxy__, }) } } - deserializer.deserialize_struct("livekit.ForwardParticipantRequest", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.GCPUpload", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for ForwardParticipantResponse { +impl serde::Serialize for GetDataBlobRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { use serde::ser::SerializeStruct; - let len = 0; - let struct_ser = serializer.serialize_struct("livekit.ForwardParticipantResponse", len)?; + let mut len = 0; + if !self.participant_identity.is_empty() { + len += 1; + } + if self.key.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.GetDataBlobRequest", len)?; + if !self.participant_identity.is_empty() { + struct_ser.serialize_field("participantIdentity", &self.participant_identity)?; + } + if let Some(v) = self.key.as_ref() { + struct_ser.serialize_field("key", v)?; + } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for ForwardParticipantResponse { +impl<'de> serde::Deserialize<'de> for GetDataBlobRequest { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ + "participant_identity", + "participantIdentity", + "key", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { + ParticipantIdentity, + Key, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -18168,7 +18957,11 @@ impl<'de> serde::Deserialize<'de> for ForwardParticipantResponse { where E: serde::de::Error, { - Ok(GeneratedField::__SkipField__) + match value { + "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), + "key" => Ok(GeneratedField::Key), + _ => Ok(GeneratedField::__SkipField__), + } } } deserializer.deserialize_identifier(GeneratedVisitor) @@ -18176,27 +18969,47 @@ impl<'de> serde::Deserialize<'de> for ForwardParticipantResponse { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = ForwardParticipantResponse; + type Value = GetDataBlobRequest; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.ForwardParticipantResponse") + formatter.write_str("struct livekit.GetDataBlobRequest") } - fn visit_map(self, mut map_: V) -> std::result::Result + fn visit_map(self, mut map_: V) -> std::result::Result where V: serde::de::MapAccess<'de>, { - while map_.next_key::()?.is_some() { - let _ = map_.next_value::()?; + let mut participant_identity__ = None; + let mut key__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::ParticipantIdentity => { + if participant_identity__.is_some() { + return Err(serde::de::Error::duplicate_field("participantIdentity")); + } + participant_identity__ = Some(map_.next_value()?); + } + GeneratedField::Key => { + if key__.is_some() { + return Err(serde::de::Error::duplicate_field("key")); + } + key__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } } - Ok(ForwardParticipantResponse { + Ok(GetDataBlobRequest { + participant_identity: participant_identity__.unwrap_or_default(), + key: key__, }) } } - deserializer.deserialize_struct("livekit.ForwardParticipantResponse", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.GetDataBlobRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for GcpUpload { +impl serde::Serialize for GetDataBlobResponse { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -18204,45 +19017,29 @@ impl serde::Serialize for GcpUpload { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.credentials.is_empty() { + if self.blob.is_some() { len += 1; } - if !self.bucket.is_empty() { - len += 1; - } - if self.proxy.is_some() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.GCPUpload", len)?; - if !self.credentials.is_empty() { - struct_ser.serialize_field("credentials", &self.credentials)?; - } - if !self.bucket.is_empty() { - struct_ser.serialize_field("bucket", &self.bucket)?; - } - if let Some(v) = self.proxy.as_ref() { - struct_ser.serialize_field("proxy", v)?; + let mut struct_ser = serializer.serialize_struct("livekit.GetDataBlobResponse", len)?; + if let Some(v) = self.blob.as_ref() { + struct_ser.serialize_field("blob", v)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for GcpUpload { +impl<'de> serde::Deserialize<'de> for GetDataBlobResponse { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "credentials", - "bucket", - "proxy", + "blob", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Credentials, - Bucket, - Proxy, + Blob, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -18265,9 +19062,7 @@ impl<'de> serde::Deserialize<'de> for GcpUpload { E: serde::de::Error, { match value { - "credentials" => Ok(GeneratedField::Credentials), - "bucket" => Ok(GeneratedField::Bucket), - "proxy" => Ok(GeneratedField::Proxy), + "blob" => Ok(GeneratedField::Blob), _ => Ok(GeneratedField::__SkipField__), } } @@ -18277,52 +19072,36 @@ impl<'de> serde::Deserialize<'de> for GcpUpload { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = GcpUpload; + type Value = GetDataBlobResponse; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.GCPUpload") + formatter.write_str("struct livekit.GetDataBlobResponse") } - fn visit_map(self, mut map_: V) -> std::result::Result + fn visit_map(self, mut map_: V) -> std::result::Result where V: serde::de::MapAccess<'de>, { - let mut credentials__ = None; - let mut bucket__ = None; - let mut proxy__ = None; + let mut blob__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Credentials => { - if credentials__.is_some() { - return Err(serde::de::Error::duplicate_field("credentials")); + GeneratedField::Blob => { + if blob__.is_some() { + return Err(serde::de::Error::duplicate_field("blob")); } - credentials__ = Some(map_.next_value()?); - } - GeneratedField::Bucket => { - if bucket__.is_some() { - return Err(serde::de::Error::duplicate_field("bucket")); - } - bucket__ = Some(map_.next_value()?); - } - GeneratedField::Proxy => { - if proxy__.is_some() { - return Err(serde::de::Error::duplicate_field("proxy")); - } - proxy__ = map_.next_value()?; + blob__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(GcpUpload { - credentials: credentials__.unwrap_or_default(), - bucket: bucket__.unwrap_or_default(), - proxy: proxy__, + Ok(GetDataBlobResponse { + blob: blob__, }) } } - deserializer.deserialize_struct("livekit.GCPUpload", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.GetDataBlobResponse", FIELDS, GeneratedVisitor) } } impl serde::Serialize for GetSipInboundTrunkRequest { @@ -29566,6 +30345,12 @@ impl serde::Serialize for PublishDataTrackRequest { if self.encryption != 0 { len += 1; } + if self.frame_encoding.is_some() { + len += 1; + } + if self.schema.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.PublishDataTrackRequest", len)?; if self.pub_handle != 0 { struct_ser.serialize_field("pubHandle", &self.pub_handle)?; @@ -29578,6 +30363,14 @@ impl serde::Serialize for PublishDataTrackRequest { .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; struct_ser.serialize_field("encryption", &v)?; } + if let Some(v) = self.frame_encoding.as_ref() { + let v = DataTrackFrameEncoding::try_from(*v) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", *v)))?; + struct_ser.serialize_field("frameEncoding", &v)?; + } + if let Some(v) = self.schema.as_ref() { + struct_ser.serialize_field("schema", v)?; + } struct_ser.end() } } @@ -29592,6 +30385,9 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { "pubHandle", "name", "encryption", + "frame_encoding", + "frameEncoding", + "schema", ]; #[allow(clippy::enum_variant_names)] @@ -29599,6 +30395,8 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { PubHandle, Name, Encryption, + FrameEncoding, + Schema, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -29624,6 +30422,8 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), "name" => Ok(GeneratedField::Name), "encryption" => Ok(GeneratedField::Encryption), + "frameEncoding" | "frame_encoding" => Ok(GeneratedField::FrameEncoding), + "schema" => Ok(GeneratedField::Schema), _ => Ok(GeneratedField::__SkipField__), } } @@ -29646,6 +30446,8 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { let mut pub_handle__ = None; let mut name__ = None; let mut encryption__ = None; + let mut frame_encoding__ = None; + let mut schema__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::PubHandle => { @@ -29668,6 +30470,18 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { } encryption__ = Some(map_.next_value::()? as i32); } + GeneratedField::FrameEncoding => { + if frame_encoding__.is_some() { + return Err(serde::de::Error::duplicate_field("frameEncoding")); + } + frame_encoding__ = map_.next_value::<::std::option::Option>()?.map(|x| x as i32); + } + GeneratedField::Schema => { + if schema__.is_some() { + return Err(serde::de::Error::duplicate_field("schema")); + } + schema__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -29677,6 +30491,8 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { pub_handle: pub_handle__.unwrap_or_default(), name: name__.unwrap_or_default(), encryption: encryption__.unwrap_or_default(), + frame_encoding: frame_encoding__, + schema: schema__, }) } } @@ -32678,6 +33494,12 @@ impl serde::Serialize for RequestResponse { request_response::Request::UnpublishDataTrack(v) => { struct_ser.serialize_field("unpublishDataTrack", v)?; } + request_response::Request::StoreDataBlob(v) => { + struct_ser.serialize_field("storeDataBlob", v)?; + } + request_response::Request::GetDataBlob(v) => { + struct_ser.serialize_field("getDataBlob", v)?; + } } } struct_ser.end() @@ -32708,6 +33530,10 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "publishDataTrack", "unpublish_data_track", "unpublishDataTrack", + "store_data_blob", + "storeDataBlob", + "get_data_blob", + "getDataBlob", ]; #[allow(clippy::enum_variant_names)] @@ -32723,6 +33549,8 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { UpdateVideoTrack, PublishDataTrack, UnpublishDataTrack, + StoreDataBlob, + GetDataBlob, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -32756,6 +33584,8 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "updateVideoTrack" | "update_video_track" => Ok(GeneratedField::UpdateVideoTrack), "publishDataTrack" | "publish_data_track" => Ok(GeneratedField::PublishDataTrack), "unpublishDataTrack" | "unpublish_data_track" => Ok(GeneratedField::UnpublishDataTrack), + "storeDataBlob" | "store_data_blob" => Ok(GeneratedField::StoreDataBlob), + "getDataBlob" | "get_data_blob" => Ok(GeneratedField::GetDataBlob), _ => Ok(GeneratedField::__SkipField__), } } @@ -32855,6 +33685,20 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { return Err(serde::de::Error::duplicate_field("unpublishDataTrack")); } request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UnpublishDataTrack) +; + } + GeneratedField::StoreDataBlob => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("storeDataBlob")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::StoreDataBlob) +; + } + GeneratedField::GetDataBlob => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("getDataBlob")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::GetDataBlob) ; } GeneratedField::__SkipField__ => { @@ -32891,6 +33735,7 @@ impl serde::Serialize for request_response::Reason { Self::InvalidName => "INVALID_NAME", Self::DuplicateHandle => "DUPLICATE_HANDLE", Self::DuplicateName => "DUPLICATE_NAME", + Self::InvalidRequest => "INVALID_REQUEST", }; serializer.serialize_str(variant) } @@ -32913,6 +33758,7 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "INVALID_NAME", "DUPLICATE_HANDLE", "DUPLICATE_NAME", + "INVALID_REQUEST", ]; struct GeneratedVisitor; @@ -32964,6 +33810,7 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "INVALID_NAME" => Ok(request_response::Reason::InvalidName), "DUPLICATE_HANDLE" => Ok(request_response::Reason::DuplicateHandle), "DUPLICATE_NAME" => Ok(request_response::Reason::DuplicateName), + "INVALID_REQUEST" => Ok(request_response::Reason::InvalidRequest), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -42797,6 +43644,12 @@ impl serde::Serialize for SignalRequest { signal_request::Message::UpdateDataSubscription(v) => { struct_ser.serialize_field("updateDataSubscription", v)?; } + signal_request::Message::StoreDataBlobRequest(v) => { + struct_ser.serialize_field("storeDataBlobRequest", v)?; + } + signal_request::Message::GetDataBlobRequest(v) => { + struct_ser.serialize_field("getDataBlobRequest", v)?; + } } } struct_ser.end() @@ -42841,6 +43694,10 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "unpublishDataTrackRequest", "update_data_subscription", "updateDataSubscription", + "store_data_blob_request", + "storeDataBlobRequest", + "get_data_blob_request", + "getDataBlobRequest", ]; #[allow(clippy::enum_variant_names)] @@ -42865,6 +43722,8 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { PublishDataTrackRequest, UnpublishDataTrackRequest, UpdateDataSubscription, + StoreDataBlobRequest, + GetDataBlobRequest, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -42907,6 +43766,8 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "publishDataTrackRequest" | "publish_data_track_request" => Ok(GeneratedField::PublishDataTrackRequest), "unpublishDataTrackRequest" | "unpublish_data_track_request" => Ok(GeneratedField::UnpublishDataTrackRequest), "updateDataSubscription" | "update_data_subscription" => Ok(GeneratedField::UpdateDataSubscription), + "storeDataBlobRequest" | "store_data_blob_request" => Ok(GeneratedField::StoreDataBlobRequest), + "getDataBlobRequest" | "get_data_blob_request" => Ok(GeneratedField::GetDataBlobRequest), _ => Ok(GeneratedField::__SkipField__), } } @@ -43066,6 +43927,20 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { return Err(serde::de::Error::duplicate_field("updateDataSubscription")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UpdateDataSubscription) +; + } + GeneratedField::StoreDataBlobRequest => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("storeDataBlobRequest")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::StoreDataBlobRequest) +; + } + GeneratedField::GetDataBlobRequest => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("getDataBlobRequest")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::GetDataBlobRequest) ; } GeneratedField::__SkipField__ => { @@ -43181,6 +44056,9 @@ impl serde::Serialize for SignalResponse { signal_response::Message::DataTrackSubscriberHandles(v) => { struct_ser.serialize_field("dataTrackSubscriberHandles", v)?; } + signal_response::Message::GetDataBlobResponse(v) => { + struct_ser.serialize_field("getDataBlobResponse", v)?; + } } } struct_ser.end() @@ -43240,6 +44118,8 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "unpublishDataTrackResponse", "data_track_subscriber_handles", "dataTrackSubscriberHandles", + "get_data_blob_response", + "getDataBlobResponse", ]; #[allow(clippy::enum_variant_names)] @@ -43272,6 +44152,7 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { PublishDataTrackResponse, UnpublishDataTrackResponse, DataTrackSubscriberHandles, + GetDataBlobResponse, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -43322,6 +44203,7 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "publishDataTrackResponse" | "publish_data_track_response" => Ok(GeneratedField::PublishDataTrackResponse), "unpublishDataTrackResponse" | "unpublish_data_track_response" => Ok(GeneratedField::UnpublishDataTrackResponse), "dataTrackSubscriberHandles" | "data_track_subscriber_handles" => Ok(GeneratedField::DataTrackSubscriberHandles), + "getDataBlobResponse" | "get_data_blob_response" => Ok(GeneratedField::GetDataBlobResponse), _ => Ok(GeneratedField::__SkipField__), } } @@ -43536,6 +44418,13 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { return Err(serde::de::Error::duplicate_field("dataTrackSubscriberHandles")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::DataTrackSubscriberHandles) +; + } + GeneratedField::GetDataBlobResponse => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("getDataBlobResponse")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::GetDataBlobResponse) ; } GeneratedField::__SkipField__ => { @@ -45100,6 +45989,101 @@ impl<'de> serde::Deserialize<'de> for StorageConfig { deserializer.deserialize_struct("livekit.StorageConfig", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for StoreDataBlobRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.blob.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.StoreDataBlobRequest", len)?; + if let Some(v) = self.blob.as_ref() { + struct_ser.serialize_field("blob", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for StoreDataBlobRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "blob", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Blob, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "blob" => Ok(GeneratedField::Blob), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = StoreDataBlobRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.StoreDataBlobRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut blob__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Blob => { + if blob__.is_some() { + return Err(serde::de::Error::duplicate_field("blob")); + } + blob__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(StoreDataBlobRequest { + blob: blob__, + }) + } + } + deserializer.deserialize_struct("livekit.StoreDataBlobRequest", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for StreamInfo { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result From d28a823002b0bc0498d5e9a0e70884d83c3a7388 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:32:04 -0700 Subject: [PATCH 02/28] Expose schema and frame encoding --- livekit-datatrack/src/lib.rs | 3 + livekit-datatrack/src/local/events.rs | 3 + livekit-datatrack/src/local/manager.rs | 18 +++ livekit-datatrack/src/local/mod.rs | 13 +- livekit-datatrack/src/local/proto.rs | 59 +++++++- livekit-datatrack/src/remote/manager.rs | 18 +++ livekit-datatrack/src/remote/proto.rs | 2 + livekit-datatrack/src/schema.rs | 184 ++++++++++++++++++++++++ livekit-datatrack/src/track.rs | 7 +- 9 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 livekit-datatrack/src/schema.rs diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index 1faadfeb5..699722255 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -17,6 +17,9 @@ /// Common types for local and remote tracks. mod track; +/// Schema and frame encoding metadata for typed tracks. +mod schema; + /// Local track publication. mod local; diff --git a/livekit-datatrack/src/local/events.rs b/livekit-datatrack/src/local/events.rs index fda9c1536..9400e3f07 100644 --- a/livekit-datatrack/src/local/events.rs +++ b/livekit-datatrack/src/local/events.rs @@ -15,6 +15,7 @@ use crate::{ api::{DataTrackInfo, DataTrackOptions, LocalDataTrack, PublishError}, packet::Handle, + schema::{DataTrackFrameEncoding, DataTrackSchemaId}, }; use bytes::Bytes; use from_variants::FromVariants; @@ -124,6 +125,8 @@ pub struct SfuPublishRequest { pub handle: Handle, pub name: String, pub uses_e2ee: bool, + pub schema: Option, + pub frame_encoding: Option, } /// Request sent to the SFU to unpublish a track. diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 17212ac0b..7deb5efc6 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -128,6 +128,8 @@ impl Manager { handle, name: event.options.name, uses_e2ee: self.encryption_provider.is_some(), + schema: event.options.schema, + frame_encoding: event.options.frame_encoding, }; _ = self.event_out_tx.send(event.into()).await; } @@ -280,6 +282,8 @@ impl Manager { handle: info.pub_handle, name: info.name.clone(), uses_e2ee: info.uses_e2ee, + schema: info.schema.clone(), + frame_encoding: info.frame_encoding, }; _ = state_tx.send(PublishState::Republishing); _ = self.event_out_tx.send(event.into()).await; @@ -525,6 +529,8 @@ mod tests { pub_handle, name: event.name, uses_e2ee: event.uses_e2ee, + schema: None, + frame_encoding: None, }; let event = SfuPublishResponse { handle: event.handle, result: Ok(info) }; _ = input.send(event.into()); @@ -604,6 +610,8 @@ mod tests { pub_handle: handle, name: "test".into(), uses_e2ee: false, + schema: None, + frame_encoding: None, }; let event = SfuPublishResponse { handle, result: Ok(info) }; input.send(event.into()).unwrap(); @@ -634,6 +642,8 @@ mod tests { pub_handle: event.handle, name: "secure".into(), uses_e2ee: true, + schema: None, + frame_encoding: None, }; let event = SfuPublishResponse { handle: event.handle, result: Ok(info) }; input.send(event.into()).unwrap(); @@ -674,6 +684,8 @@ mod tests { pub_handle: handle, name: track_name.clone(), uses_e2ee: false, + schema: None, + frame_encoding: None, }; let event = SfuPublishResponse { handle, result: Ok(info) }; input.send(event.into()).unwrap(); @@ -699,6 +711,8 @@ mod tests { pub_handle: handle, name: track_name.clone(), uses_e2ee: false, + schema: None, + frame_encoding: None, }; let event = SfuPublishResponse { handle, result: Ok(info) }; input.send(event.into()).unwrap(); @@ -728,6 +742,8 @@ mod tests { pub_handle: event.handle, name: name.into(), uses_e2ee: false, + schema: None, + frame_encoding: None, }; let event = SfuPublishResponse { handle: event.handle, result: Ok(info) }; input.send(event.into()).unwrap(); @@ -767,6 +783,8 @@ mod tests { pub_handle: event.handle, name: "active".into(), uses_e2ee: false, + schema: None, + frame_encoding: None, }; let event = SfuPublishResponse { handle: event.handle, result: Ok(info) }; input.send(event.into()).unwrap(); diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 358547146..4c432cd49 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -14,6 +14,7 @@ use crate::{ api::{DataTrack, DataTrackFrame, DataTrackInfo, InternalError}, + schema::{DataTrackFrameEncoding, DataTrackSchemaId}, track::DataTrackInner, }; use std::{fmt, marker::PhantomData, sync::Arc}; @@ -153,6 +154,8 @@ impl Drop for LocalTrackInner { #[derive(Clone, Debug)] pub struct DataTrackOptions { pub(crate) name: String, + pub(crate) schema: Option, + pub(crate) frame_encoding: Option, } impl DataTrackOptions { @@ -165,7 +168,15 @@ impl DataTrackOptions { /// - Must be unique per publisher /// pub fn new(name: impl Into) -> Self { - Self { name: name.into() } + Self { name: name.into(), schema: None, frame_encoding: None } + } + + pub fn with_schema(self, schema: DataTrackSchemaId) -> Self { + Self { schema: Some(schema), ..self } + } + + pub fn with_frame_encoding(self, encoding: DataTrackFrameEncoding) -> Self { + Self { frame_encoding: Some(encoding), ..self } } } diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index 907250681..feba742e9 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -22,6 +22,7 @@ use super::events::*; use crate::{ api::{DataTrackInfo, DataTrackSid, InternalError, PublishError}, packet::Handle, + schema::DataTrackFrameEncoding, }; use anyhow::{anyhow, Context}; use livekit_protocol as proto; @@ -33,7 +34,17 @@ impl From for proto::PublishDataTrackRequest { fn from(event: SfuPublishRequest) -> Self { use proto::encryption::Type; let encryption = if event.uses_e2ee { Type::Gcm } else { Type::None }.into(); - Self { pub_handle: event.handle.into(), name: event.name, encryption } + let schema = event.schema.map(|schema| schema.into()); + let frame_encoding = event + .frame_encoding + .map(|encoding| Into::::into(encoding).into()); + Self { + pub_handle: event.handle.into(), + name: event.name, + encryption, + schema, + frame_encoding, + } } } @@ -74,8 +85,24 @@ impl TryFrom for DataTrackInfo { proto::encryption::Type::Gcm => true, other => Err(anyhow!("Unsupported E2EE type: {:?}", other))?, }; + let frame_encoding = msg + .frame_encoding + .is_some() + .then(|| Into::::into(msg.frame_encoding()).into()) + .flatten(); + let sid: DataTrackSid = msg.sid.try_into().map_err(anyhow::Error::from)?; - Ok(Self { pub_handle: handle, sid: RwLock::new(sid).into(), name: msg.name, uses_e2ee }) + + let schema = msg.schema.map(|schema| schema.into()); + + Ok(Self { + pub_handle: handle, + sid: RwLock::new(sid).into(), + name: msg.name, + uses_e2ee, + schema, + frame_encoding, + }) } } @@ -106,12 +133,19 @@ impl From for proto::DataTrackInfo { proto::encryption::Type::Gcm } else { proto::encryption::Type::None - }; + } as i32; + let sid = info.sid().to_string(); + let schema = info.schema.map(|schema| schema.into()); + let frame_encoding = info + .frame_encoding + .map(|encoding| Into::::into(encoding).into()); Self { pub_handle: info.pub_handle.into(), - sid: info.sid().to_string(), + sid, name: info.name, - encryption: encryption as i32, + encryption, + schema, + frame_encoding, } } } @@ -128,6 +162,8 @@ pub fn publish_responses_for_sync_state( #[cfg(test)] mod tests { + use crate::schema::{DataTrackFrameEncoding, DataTrackSchemaEncoding, DataTrackSchemaId}; + use super::*; use fake::{Fake, Faker}; @@ -137,6 +173,8 @@ mod tests { handle: 1u32.try_into().unwrap(), name: "track".into(), uses_e2ee: true, + schema: None, + frame_encoding: None, }; let request: proto::PublishDataTrackRequest = event.into(); assert_eq!(request.pub_handle, 1); @@ -159,6 +197,12 @@ mod tests { sid: "DTR_1234".into(), name: "track".into(), encryption: proto::encryption::Type::Gcm.into(), + schema: proto::DataTrackSchemaId { + name: "schema".into(), + encoding: proto::DataTrackSchemaEncoding::JsonSchema.into(), + } + .into(), + frame_encoding: Some(proto::DataTrackFrameEncoding::Json.into()), } .into(), }; @@ -169,6 +213,11 @@ mod tests { assert_eq!(info.pub_handle, 1u32.try_into().unwrap()); assert_eq!(*info.sid.read().unwrap(), "DTR_1234".to_string().try_into().unwrap()); assert_eq!(info.name, "track"); + assert_eq!( + info.schema, + Some(DataTrackSchemaId::new("schema", DataTrackSchemaEncoding::JsonSchema)) + ); + assert_eq!(info.frame_encoding, Some(DataTrackFrameEncoding::Json)); assert!(info.uses_e2ee); } diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index ce1bb260f..947a41513 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -596,6 +596,8 @@ mod tests { pub_handle: Faker.fake(), // Pub handle name: track_name.clone(), uses_e2ee: false, + schema: None, + frame_encoding: None, }], )]), }; @@ -658,6 +660,8 @@ mod tests { pub_handle: Faker.fake(), name: "test".into(), uses_e2ee: false, + schema: None, + frame_encoding: None, }; // Simulate track published @@ -694,6 +698,8 @@ mod tests { pub_handle: Faker.fake(), name: "test".into(), uses_e2ee: false, + schema: None, + frame_encoding: None, }; // Simulate three identical publication updates @@ -726,6 +732,8 @@ mod tests { pub_handle: Faker.fake(), name: "test".into(), uses_e2ee: false, + schema: None, + frame_encoding: None, }; // Simulate track published @@ -785,6 +793,8 @@ mod tests { pub_handle: Faker.fake(), name: "test".into(), uses_e2ee: true, + schema: None, + frame_encoding: None, }; // Simulate track published (with e2ee) @@ -846,6 +856,8 @@ mod tests { pub_handle: Faker.fake(), name: "test".into(), uses_e2ee: false, + schema: None, + frame_encoding: None, }; // Simulate track published @@ -944,6 +956,8 @@ mod tests { pub_handle: Faker.fake(), name: "test".into(), uses_e2ee: false, + schema: None, + frame_encoding: None, }; // Simulate track published @@ -987,6 +1001,8 @@ mod tests { pub_handle: Faker.fake(), name: "test".into(), uses_e2ee: false, + schema: None, + frame_encoding: None, }; // Simulate track published @@ -1038,6 +1054,8 @@ mod tests { pub_handle: Faker.fake(), name: "test".into(), uses_e2ee: false, + schema: None, + frame_encoding: None, }; // Simulate track published diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index 6f1b29d16..1a382b0e9 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -150,6 +150,8 @@ mod tests { sid: "DTR_1234".into(), name: "track1".into(), encryption: proto::encryption::Type::Gcm.into(), + schema: None, + frame_encoding: None, }]; let mut participant_info = proto::ParticipantInfo { data_tracks, ..Default::default() }; diff --git a/livekit-datatrack/src/schema.rs b/livekit-datatrack/src/schema.rs new file mode 100644 index 000000000..5b9d563cd --- /dev/null +++ b/livekit-datatrack/src/schema.rs @@ -0,0 +1,184 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use livekit_protocol as proto; +use std::sync::Arc; + +/// Identifier for a data track schema. +/// +/// A schema ID is a compound identifier consisting of two components: +/// - Name (e.g. "joint_positions") +/// - Encoding +/// +/// Two schema IDs with the same name but different encodings are not equivalent. +/// +/// Clones of this type are cheap since the name component is reference counted. +/// +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct DataTrackSchemaId { + name: Arc, + encoding: DataTrackSchemaEncoding, +} + +impl DataTrackSchemaId { + /// Creates a new schema ID. + pub fn new(name: impl Into, encoding: DataTrackSchemaEncoding) -> Self { + Self { name: Arc::::from(name.into()), encoding } + } + + /// Returns the name component of the ID. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the encoding component of the ID. + pub fn encoding(&self) -> DataTrackSchemaEncoding { + self.encoding + } +} + +/// Encoding used for a schema definition. +/// +/// See also: [`DataTrackSchemaId`] +/// +#[non_exhaustive] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[cfg_attr(test, derive(fake::Dummy))] +pub enum DataTrackSchemaEncoding { + /// Protocol Buffer IDL: describes `PROTOBUF` frame encoding. + Protobuf, + /// FlatBuffer IDL: describes `FLATBUFFER` frame encoding. + Flatbuffer, + /// ROS 1 Message: describes `ROS1` frame encoding. + Ros1Msg, + /// ROS 2 Message: describes `CDR` frame encoding. + Ros2Msg, + /// ROS 2 IDL: describes `CDR` frame encoding. + Ros2Idl, + /// OMG IDL: describes `CDR` frame encoding. + OmgIdl, + /// JSON Schema: describes `JSON` frame encoding. + JsonSchema, + /// Another encoding not known to this client version. + Other, +} + +/// Encoding used for frames pushed on a data track. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[cfg_attr(test, derive(fake::Dummy))] +pub enum DataTrackFrameEncoding { + /// ROS 1: must be described by `ROS1_MSG` schema encoding. + Ros1, + /// CDR: must be described by `ROS2_MSG`, `ROS2_IDL`, or `OMG_IDL` schema encoding. + Cdr, + /// Protocol Buffer: must be described by `PROTOBUF` schema encoding. + Protobuf, + /// FlatBuffer: must be described by `FLATBUFFER` schema encoding. + Flatbuffer, + /// CBOR: self-describing. + Cbor, + /// MessagePack: self-describing. + Msgpack, + /// JSON: self-describing or described by `JSON_SCHEMA` schema encoding. + Json, + /// Another encoding not known to this client version. + Other, +} + +impl From for DataTrackSchemaId { + fn from(msg: proto::DataTrackSchemaId) -> Self { + let encoding = msg.encoding().into(); + DataTrackSchemaId::new(msg.name, encoding) + } +} + +impl From for proto::DataTrackSchemaId { + fn from(value: DataTrackSchemaId) -> Self { + Self { + name: value.name.to_string(), + encoding: Into::::into(value.encoding).into(), + } + } +} + +impl From for DataTrackSchemaEncoding { + fn from(msg: proto::DataTrackSchemaEncoding) -> Self { + match msg { + proto::DataTrackSchemaEncoding::Unspecified => Self::Other, + proto::DataTrackSchemaEncoding::Protobuf => Self::Protobuf, + proto::DataTrackSchemaEncoding::Flatbuffer => Self::Flatbuffer, + proto::DataTrackSchemaEncoding::Ros1Msg => Self::Ros1Msg, + proto::DataTrackSchemaEncoding::Ros2Msg => Self::Ros2Msg, + proto::DataTrackSchemaEncoding::Ros2Idl => Self::Ros2Idl, + proto::DataTrackSchemaEncoding::OmgIdl => Self::OmgIdl, + proto::DataTrackSchemaEncoding::JsonSchema => Self::JsonSchema, + } + } +} + +impl From for proto::DataTrackSchemaEncoding { + fn from(value: DataTrackSchemaEncoding) -> Self { + match value { + DataTrackSchemaEncoding::Other => Self::Unspecified, + DataTrackSchemaEncoding::Protobuf => Self::Protobuf, + DataTrackSchemaEncoding::Flatbuffer => Self::Flatbuffer, + DataTrackSchemaEncoding::Ros1Msg => Self::Ros1Msg, + DataTrackSchemaEncoding::Ros2Msg => Self::Ros2Msg, + DataTrackSchemaEncoding::Ros2Idl => Self::Ros2Idl, + DataTrackSchemaEncoding::OmgIdl => Self::OmgIdl, + DataTrackSchemaEncoding::JsonSchema => Self::JsonSchema, + } + } +} + +impl From for DataTrackFrameEncoding { + fn from(msg: proto::DataTrackFrameEncoding) -> Self { + match msg { + proto::DataTrackFrameEncoding::Unspecified => Self::Other, + proto::DataTrackFrameEncoding::Ros1 => Self::Ros1, + proto::DataTrackFrameEncoding::Cdr => Self::Cdr, + proto::DataTrackFrameEncoding::Protobuf => Self::Protobuf, + proto::DataTrackFrameEncoding::Flatbuffer => Self::Flatbuffer, + proto::DataTrackFrameEncoding::Cbor => Self::Cbor, + proto::DataTrackFrameEncoding::Msgpack => Self::Msgpack, + proto::DataTrackFrameEncoding::Json => Self::Json, + } + } +} + +impl From for proto::DataTrackFrameEncoding { + fn from(value: DataTrackFrameEncoding) -> Self { + match value { + DataTrackFrameEncoding::Other => Self::Unspecified, + DataTrackFrameEncoding::Ros1 => Self::Ros1, + DataTrackFrameEncoding::Cdr => Self::Cdr, + DataTrackFrameEncoding::Protobuf => Self::Protobuf, + DataTrackFrameEncoding::Flatbuffer => Self::Flatbuffer, + DataTrackFrameEncoding::Cbor => Self::Cbor, + DataTrackFrameEncoding::Msgpack => Self::Msgpack, + DataTrackFrameEncoding::Json => Self::Json, + } + } +} + +#[cfg(test)] +impl fake::Dummy for DataTrackSchemaId { + fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { + use fake::{Fake, Faker}; + let name: String = Faker.fake_with_rng(rng); + let encoding: DataTrackSchemaEncoding = Faker.fake_with_rng(rng); + Self::new(name, encoding) + } +} diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index 8c313b2b1..59e92a8ec 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::packet::Handle; +use crate::{ + packet::Handle, + schema::{DataTrackFrameEncoding, DataTrackSchemaId}, +}; use from_variants::FromVariants; use std::{ fmt::Display, @@ -71,6 +74,8 @@ pub struct DataTrackInfo { pub(crate) pub_handle: Handle, pub(crate) name: String, pub(crate) uses_e2ee: bool, + pub(crate) schema: Option, + pub(crate) frame_encoding: Option, } impl DataTrackInfo { From 419e094ec2f21285da57b4e8227c7155fb992cde Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:40:53 -0700 Subject: [PATCH 03/28] Clean up conversion --- livekit-datatrack/src/local/proto.rs | 13 +++---------- livekit-datatrack/src/schema.rs | 2 +- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index feba742e9..697e6ef1c 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -22,7 +22,6 @@ use super::events::*; use crate::{ api::{DataTrackInfo, DataTrackSid, InternalError, PublishError}, packet::Handle, - schema::DataTrackFrameEncoding, }; use anyhow::{anyhow, Context}; use livekit_protocol as proto; @@ -37,7 +36,7 @@ impl From for proto::PublishDataTrackRequest { let schema = event.schema.map(|schema| schema.into()); let frame_encoding = event .frame_encoding - .map(|encoding| Into::::into(encoding).into()); + .map(|encoding| proto::DataTrackFrameEncoding::from(encoding) as i32); Self { pub_handle: event.handle.into(), name: event.name, @@ -85,14 +84,8 @@ impl TryFrom for DataTrackInfo { proto::encryption::Type::Gcm => true, other => Err(anyhow!("Unsupported E2EE type: {:?}", other))?, }; - let frame_encoding = msg - .frame_encoding - .is_some() - .then(|| Into::::into(msg.frame_encoding()).into()) - .flatten(); - + let frame_encoding = msg.frame_encoding.map(|_| msg.frame_encoding().into()); let sid: DataTrackSid = msg.sid.try_into().map_err(anyhow::Error::from)?; - let schema = msg.schema.map(|schema| schema.into()); Ok(Self { @@ -138,7 +131,7 @@ impl From for proto::DataTrackInfo { let schema = info.schema.map(|schema| schema.into()); let frame_encoding = info .frame_encoding - .map(|encoding| Into::::into(encoding).into()); + .map(|encoding| proto::DataTrackFrameEncoding::from(encoding) as i32); Self { pub_handle: info.pub_handle.into(), sid, diff --git a/livekit-datatrack/src/schema.rs b/livekit-datatrack/src/schema.rs index 5b9d563cd..52712bbc3 100644 --- a/livekit-datatrack/src/schema.rs +++ b/livekit-datatrack/src/schema.rs @@ -108,7 +108,7 @@ impl From for proto::DataTrackSchemaId { fn from(value: DataTrackSchemaId) -> Self { Self { name: value.name.to_string(), - encoding: Into::::into(value.encoding).into(), + encoding: proto::DataTrackSchemaEncoding::from(value.encoding) as i32, } } } From 2099af1071257b51b9633d4e75bdefe524176ea2 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:45:29 -0700 Subject: [PATCH 04/28] Add unit test --- livekit-datatrack/src/local/proto.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index 697e6ef1c..d6f5b9584 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -214,6 +214,28 @@ mod tests { assert!(info.uses_e2ee); } + #[test] + fn test_frame_encoding_mapping() { + let base = proto::DataTrackInfo { + pub_handle: 1, + sid: "DTR_1234".into(), + name: "track".into(), + encryption: proto::encryption::Type::None.into(), + schema: None, + frame_encoding: None, + }; + + let info: DataTrackInfo = base.clone().try_into().unwrap(); + assert_eq!(info.frame_encoding, None); + + let unspecified = proto::DataTrackInfo { + frame_encoding: Some(proto::DataTrackFrameEncoding::Unspecified.into()), + ..base + }; + let info: DataTrackInfo = unspecified.try_into().unwrap(); + assert_eq!(info.frame_encoding, Some(DataTrackFrameEncoding::Other)); + } + #[test] fn test_from_request_response() { use proto::request_response::{Reason, Request}; From 0a48860c8916fad913b64257d8e63970d986b55a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:59:11 -0700 Subject: [PATCH 05/28] Document encoding enum cases --- livekit-datatrack/src/schema.rs | 54 ++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/livekit-datatrack/src/schema.rs b/livekit-datatrack/src/schema.rs index 52712bbc3..3f55f8b9c 100644 --- a/livekit-datatrack/src/schema.rs +++ b/livekit-datatrack/src/schema.rs @@ -56,19 +56,33 @@ impl DataTrackSchemaId { #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] #[cfg_attr(test, derive(fake::Dummy))] pub enum DataTrackSchemaEncoding { - /// Protocol Buffer IDL: describes `PROTOBUF` frame encoding. + /// Protocol Buffer IDL, describes [`Protobuf`] encoded frames. + /// + /// [`Protobuf`]: DataTrackFrameEncoding::Protobuf Protobuf, - /// FlatBuffer IDL: describes `FLATBUFFER` frame encoding. + /// FlatBuffer IDL, describes [`Flatbuffer`] encoded frames. + /// + /// [`Flatbuffer`]: DataTrackFrameEncoding::Flatbuffer Flatbuffer, - /// ROS 1 Message: describes `ROS1` frame encoding. + /// ROS 1 Message, describes [`Ros1`] encoded frames. + /// + /// [`Ros1`]: DataTrackFrameEncoding::Ros1 Ros1Msg, - /// ROS 2 Message: describes `CDR` frame encoding. + /// ROS 2 Message, describes [`Cdr`] encoded frames. + /// + /// [`Cdr`]: DataTrackFrameEncoding::Cdr Ros2Msg, - /// ROS 2 IDL: describes `CDR` frame encoding. + /// ROS 2 IDL, describes [`Cdr`] encoded frames. + /// + /// [`Cdr`]: DataTrackFrameEncoding::Cdr Ros2Idl, - /// OMG IDL: describes `CDR` frame encoding. + /// OMG IDL, describes [`Cdr`] encoded frames. + /// + /// [`Cdr`]: DataTrackFrameEncoding::Cdr OmgIdl, - /// JSON Schema: describes `JSON` frame encoding. + /// JSON Schema, describes [`Json`] encoded frames. + /// + /// [`Json`]: DataTrackFrameEncoding::Json JsonSchema, /// Another encoding not known to this client version. Other, @@ -79,19 +93,31 @@ pub enum DataTrackSchemaEncoding { #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] #[cfg_attr(test, derive(fake::Dummy))] pub enum DataTrackFrameEncoding { - /// ROS 1: must be described by `ROS1_MSG` schema encoding. + /// ROS 1, must be described by a [`Ros1Msg`] schema. + /// + /// [`Ros1Msg`]: DataTrackSchemaEncoding::Ros1Msg Ros1, - /// CDR: must be described by `ROS2_MSG`, `ROS2_IDL`, or `OMG_IDL` schema encoding. + /// CDR, must be described by a [`Ros2Msg`], [`Ros2Idl`], or [`OmgIdl`] schema. + /// + /// [`Ros2Msg`]: DataTrackSchemaEncoding::Ros2Msg + /// [`Ros2Idl`]: DataTrackSchemaEncoding::Ros2Idl + /// [`OmgIdl`]: DataTrackSchemaEncoding::OmgIdl Cdr, - /// Protocol Buffer: must be described by `PROTOBUF` schema encoding. + /// Protocol Buffer, must be described by a [`Protobuf`] schema. + /// + /// [`Protobuf`]: DataTrackSchemaEncoding::Protobuf Protobuf, - /// FlatBuffer: must be described by `FLATBUFFER` schema encoding. + /// FlatBuffer, must be described by a [`Flatbuffer`] schema. + /// + /// [`Flatbuffer`]: DataTrackSchemaEncoding::Flatbuffer Flatbuffer, - /// CBOR: self-describing. + /// CBOR, self-describing. Cbor, - /// MessagePack: self-describing. + /// MessagePack, self-describing. Msgpack, - /// JSON: self-describing or described by `JSON_SCHEMA` schema encoding. + /// JSON, self-describing or described by a [`JsonSchema`] schema. + /// + /// [`JsonSchema`]: DataTrackSchemaEncoding::JsonSchema Json, /// Another encoding not known to this client version. Other, From d5832d8642f4eea94f0e6d400420d7fd3d74777f Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:03:05 -0700 Subject: [PATCH 06/28] Export under api module --- livekit-datatrack/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index 699722255..88db850a1 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -43,7 +43,7 @@ mod error; /// Public APIs re-exported by client SDKs. pub mod api { - pub use crate::{error::*, frame::*, local::*, remote::*, track::*}; + pub use crate::{error::*, frame::*, local::*, remote::*, schema::*, track::*}; } /// Internal APIs used within client SDKs to power data tracks functionality. From 327140d38121d6d54329ba7ec6d110f125bbc7bd Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:20:23 -0700 Subject: [PATCH 07/28] Define conversion to data blob key --- livekit-datatrack/src/schema.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/livekit-datatrack/src/schema.rs b/livekit-datatrack/src/schema.rs index 3f55f8b9c..f1e159426 100644 --- a/livekit-datatrack/src/schema.rs +++ b/livekit-datatrack/src/schema.rs @@ -199,6 +199,12 @@ impl From for proto::DataTrackFrameEncoding { } } +impl From for proto::DataBlobKey { + fn from(id: DataTrackSchemaId) -> Self { + Self { key: Some(proto::data_blob_key::Key::SchemaId(id.into())) } + } +} + #[cfg(test)] impl fake::Dummy for DataTrackSchemaId { fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { From 0099197a5a8ba6dbd8caef8779010aa46691f96f Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:57:17 -0700 Subject: [PATCH 08/28] Prototype --- .../src/room/participant/local_participant.rs | 25 ++++++- livekit/src/rtc_engine/mod.rs | 73 ++++++++++++++++++- livekit/src/rtc_engine/rtc_session.rs | 22 ++++++ 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index e58d172df..2613f0b0b 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -31,7 +31,7 @@ use crate::{ ByteStreamInfo, ByteStreamWriter, StreamByteOptions, StreamResult, StreamTextOptions, TextStreamInfo, TextStreamWriter, }, - data_track::{self, DataTrack, DataTrackOptions, Local}, + data_track::{self, DataTrack, DataTrackOptions, DataTrackSchemaId, Local}, e2ee::EncryptionType, options::{self, compute_video_encodings, video_layers_from_encodings, TrackPublishOptions}, prelude::*, @@ -917,6 +917,29 @@ impl LocalParticipant { pub fn update_data_encryption_status(&self, _is_encrypted: bool) { // Local participants don't receive data messages, so this is a no-op } + + pub async fn define_schema(&self, id: DataTrackSchemaId, definition: String) -> RoomResult<()> { + self.inner.rtc_engine.store_data_blob(id.into(), definition.into()).await?; + Ok(()) + } + + pub async fn get_schema( + &self, + id: DataTrackSchemaId, + participant: ParticipantIdentity, + ) -> RoomResult { + let contents = self + .inner + .rtc_engine + .get_data_blob(id.into(), participant) + .await + .inspect_err(|err| log::error!("Failed to get schema: {err}"))?; + + let definition = String::from_utf8(contents.to_vec()).map_err(|err| { + RoomError::Internal(format!("schema definition is not valid UTF-8: {err}")) + })?; + Ok(definition) + } } #[cfg(test)] diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index f560ef554..244e12dd5 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +use bytes::Bytes; use libwebrtc::prelude::*; use livekit_api::signal_client::{SignalError, SignalOptions}; use livekit_datatrack::backend as dt; use livekit_protocol as proto; -use livekit_runtime::{interval, Interval, JoinHandle, MissedTickBehavior}; +use livekit_runtime::{interval, timeout, Interval, JoinHandle, MissedTickBehavior}; use parking_lot::{RwLock, RwLockReadGuard}; use std::{borrow::Cow, fmt::Debug, sync::Arc, time::Duration}; use thiserror::Error; @@ -284,6 +285,71 @@ impl RtcEngine { session.publish_data(data, kind, is_raw_packet).await } + // TODO: unify request/response logic, timeout behavior across SDK. + const DATA_BLOB_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); + + pub async fn store_data_blob( + &self, + key: proto::DataBlobKey, + contents: Bytes, + ) -> EngineResult<()> { + let blob = proto::DataBlob { key: Some(key), contents: contents.into() }; + + let request_id = self.session().signal_client().next_request_id(); + // TODO: `StoreDataBlobRequest` is missing a `request_id` field. Until it is added + // (and echoed back by the server on the `RequestResponse`), the response awaited + // below cannot be correlated to this request and the call will time out. + // Set `request_id` on the request once the proto field exists. + let request = proto::StoreDataBlobRequest { blob: Some(blob) }; + self.send_request(proto::signal_request::Message::StoreDataBlobRequest(request)).await; + + let response = timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, self.get_response(request_id)) + .await + .map_err(|_| { + EngineError::Signal(SignalError::Timeout( + "store data blob request timed out".into(), + )) + })?; + + match response.reason() { + proto::request_response::Reason::Ok => Ok(()), + reason => Err(EngineError::Internal( + format!("store data blob request failed ({reason:?}): {}", response.message).into(), + )), + } + } + + pub async fn get_data_blob( + &self, + key: proto::DataBlobKey, + participant: ParticipantIdentity, + ) -> EngineResult { + let request_id = self.session().signal_client().next_request_id(); + // TODO: `GetDataBlobRequest` and `GetDataBlobResponse` are both missing a + // `request_id` field. Until they are added (and echoed back by the server on the + // response), the response awaited below cannot be correlated to this request and + // the call will time out. Set `request_id` on the request once the proto field + // exists, and resolve the pending request in the `GetDataBlobResponse` handler in + // `rtc_session.rs`. + let request = + proto::GetDataBlobRequest { key: Some(key), participant_identity: participant.0 }; + self.send_request(proto::signal_request::Message::GetDataBlobRequest(request)).await; + + let response = + timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, self.get_data_blob_response(request_id)) + .await + .map_err(|_| { + EngineError::Signal(SignalError::Timeout( + "get data blob request timed out".into(), + )) + })?; + + let blob = response.blob.ok_or_else(|| { + EngineError::Internal("get data blob response is missing the blob".into()) + })?; + Ok(blob.contents.into()) + } + pub async fn simulate_scenario(&self, scenario: SimulateScenario) -> EngineResult<()> { let (session, _r_lock) = { let (handle, _r_lock) = self.inner.wait_reconnection().await?; @@ -378,6 +444,11 @@ impl RtcEngine { session.get_response(request_id).await } + pub async fn get_data_blob_response(&self, request_id: u32) -> proto::GetDataBlobResponse { + let session = self.inner.running_handle.read().session.clone(); + session.get_data_blob_response(request_id).await + } + pub async fn get_stats(&self) -> EngineResult { let session = self.inner.running_handle.read().session.clone(); session.get_stats().await diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index eb8d12a8f..2abcc5d4d 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -394,6 +394,7 @@ struct SessionInner { negotiation_queue: NegotiationQueue, pending_requests: Mutex>>, + pending_data_blob_requests: Mutex>>, e2ee_manager: Option, subscriber_primary: bool, @@ -607,6 +608,7 @@ impl RtcSession { negotiation_debouncer: Default::default(), negotiation_queue: NegotiationQueue::new(), pending_requests: Default::default(), + pending_data_blob_requests: Default::default(), e2ee_manager, subscriber_primary, pc_state_notify: Notify::new(), @@ -909,6 +911,10 @@ impl RtcSession { pub async fn get_response(&self, request_id: u32) -> proto::RequestResponse { self.inner.get_response(request_id).await } + + pub async fn get_data_blob_response(&self, request_id: u32) -> proto::GetDataBlobResponse { + self.inner.get_data_blob_response(request_id).await + } } impl SessionInner { @@ -1332,6 +1338,16 @@ impl SessionInner { self.handle_media_sections_requirement(req)?; } } + proto::signal_response::Message::GetDataBlobResponse(_response) => { + // TODO: `GetDataBlobResponse` is missing a `request_id` field, so the + // response cannot be correlated with the originating `GetDataBlobRequest`. + // Once the field exists, resolve the matching pending request, e.g.: + // if let Some(tx) = + // self.pending_data_blob_requests.lock().remove(&_response.request_id) + // { + // let _ = tx.send(_response); + // } + } _ => {} } @@ -2248,6 +2264,12 @@ impl SessionInner { self.pending_requests.lock().insert(request_id, tx); rx.await.unwrap() } + + async fn get_data_blob_response(&self, request_id: u32) -> proto::GetDataBlobResponse { + let (tx, rx) = oneshot::channel(); + self.pending_data_blob_requests.lock().insert(request_id, tx); + rx.await.expect("data blob response sender dropped") + } } /// Emit incoming data track packets as session events. From 48cd8a109aa97413a441afcb6f53d59037c8e8f0 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:08:05 -0700 Subject: [PATCH 09/28] Expose over FFI --- livekit-ffi/protocol/data_track.proto | 57 ++++++++++++++++++++++++ livekit-ffi/protocol/ffi.proto | 31 +++++++++---- livekit-ffi/src/conversion/data_track.rs | 54 +++++++++++++++++++++- livekit-ffi/src/server/participant.rs | 42 +++++++++++++++++ livekit-ffi/src/server/requests.rs | 20 +++++++++ 5 files changed, 193 insertions(+), 11 deletions(-) diff --git a/livekit-ffi/protocol/data_track.proto b/livekit-ffi/protocol/data_track.proto index 02eafdd94..ce034f410 100644 --- a/livekit-ffi/protocol/data_track.proto +++ b/livekit-ffi/protocol/data_track.proto @@ -246,3 +246,60 @@ message DataTrackStreamEOS { // Absent if the stream ended normally (i.e., due to the track being unpublished). optional SubscribeDataTrackError error = 1; } + +// MARK: - Schemas + +// Encoding used to interpret a data track schema definition. +enum DataTrackSchemaEncoding { + DATA_TRACK_SCHEMA_ENCODING_PROTOBUF = 0; + DATA_TRACK_SCHEMA_ENCODING_FLATBUFFER = 1; + DATA_TRACK_SCHEMA_ENCODING_ROS1_MSG = 2; + DATA_TRACK_SCHEMA_ENCODING_ROS2_MSG = 3; + DATA_TRACK_SCHEMA_ENCODING_ROS2_IDL = 4; + DATA_TRACK_SCHEMA_ENCODING_OMG_IDL = 5; + DATA_TRACK_SCHEMA_ENCODING_JSON_SCHEMA = 6; + DATA_TRACK_SCHEMA_ENCODING_OTHER = 7; +} + +// Uniquely identifies a data track schema. +message DataTrackSchemaId { + // Name component of the schema identifier. + required string name = 1; + // Encoding of the schema definition. + required DataTrackSchemaEncoding encoding = 2; +} + +// Define (store) a schema definition for the local participant. +message DefineSchemaRequest { + required uint64 local_participant_handle = 1; + required DataTrackSchemaId schema_id = 2; + required string definition = 3; + optional uint64 request_async_id = 4; +} +message DefineSchemaResponse { + required uint64 async_id = 1; +} +message DefineSchemaCallback { + required uint64 async_id = 1; + // Present if the schema could not be defined. + optional string error = 2; +} + +// Retrieve a schema definition previously stored by a participant. +message GetSchemaRequest { + required uint64 local_participant_handle = 1; + required DataTrackSchemaId schema_id = 2; + // Identity of the participant who owns the schema. + required string participant_identity = 3; + optional uint64 request_async_id = 4; +} +message GetSchemaResponse { + required uint64 async_id = 1; +} +message GetSchemaCallback { + required uint64 async_id = 1; + // The schema definition, present on success. + optional string definition = 2; + // Present if the schema could not be retrieved. + optional string error = 3; +} diff --git a/livekit-ffi/protocol/ffi.proto b/livekit-ffi/protocol/ffi.proto index b630095e4..53ea782bd 100644 --- a/livekit-ffi/protocol/ffi.proto +++ b/livekit-ffi/protocol/ffi.proto @@ -15,18 +15,19 @@ syntax = "proto2"; package livekit.proto; -option csharp_namespace = "LiveKit.Proto"; +import "audio_frame.proto"; +import "data_stream.proto"; +import "data_track.proto"; // import "handle.proto"; import "e2ee.proto"; +import "room.proto"; +import "rpc.proto"; import "track.proto"; import "track_publication.proto"; -import "room.proto"; import "video_frame.proto"; -import "audio_frame.proto"; -import "rpc.proto"; -import "data_stream.proto"; -import "data_track.proto"; + +option csharp_namespace = "LiveKit.Proto"; // **How is the livekit-ffi working: // We refer as the ffi server the Rust server that is running the LiveKit client implementation, and we @@ -165,6 +166,10 @@ message FfiRequest { DataTrackStreamReadRequest data_track_stream_read = 75; RemoteDataTrackSetPipelineOptionsRequest remote_data_track_set_pipeline_options = 84; + // Data Track (schemas) + DefineSchemaRequest define_schema = 85; + GetSchemaRequest get_schema = 86; + // Reconnection / chaos testing SimulateScenarioRequest simulate_scenario = 76; @@ -179,7 +184,7 @@ message FfiRequest { // Room event ready signal ReadyForRoomEventRequest ready_for_room_event = 83; - // NEXT_ID: 85 + // NEXT_ID: 87 } } @@ -290,6 +295,10 @@ message FfiResponse { DataTrackStreamReadResponse data_track_stream_read = 74; RemoteDataTrackSetPipelineOptionsResponse remote_data_track_set_pipeline_options = 84; + // Data Track (schemas) + DefineSchemaResponse define_schema = 85; + GetSchemaResponse get_schema = 86; + // Reconnection / chaos testing SimulateScenarioResponse simulate_scenario = 75; @@ -304,7 +313,7 @@ message FfiResponse { // Room event ready signal ReadyForRoomEventResponse ready_for_room_event = 82; - // NEXT_ID: 85 + // NEXT_ID: 87 } } @@ -369,7 +378,11 @@ message FfiEvent { SimulateScenarioCallback simulate_scenario = 44; - // NEXT_ID: 45 + // Data Track (schemas) + DefineSchemaCallback define_schema = 45; + GetSchemaCallback get_schema = 46; + + // NEXT_ID: 47 } } diff --git a/livekit-ffi/src/conversion/data_track.rs b/livekit-ffi/src/conversion/data_track.rs index e4777a680..7b668905b 100644 --- a/livekit-ffi/src/conversion/data_track.rs +++ b/livekit-ffi/src/conversion/data_track.rs @@ -15,8 +15,9 @@ use crate::proto; use livekit::{ data_track::{ - DataTrackFrame, DataTrackInfo, DataTrackOptions, DataTrackSubscribeError, PublishError, - PushFrameError, PushFrameErrorReason, RemoteDataTrackPipelineOptions, + DataTrackFrame, DataTrackInfo, DataTrackOptions, DataTrackSchemaEncoding, + DataTrackSchemaId, DataTrackSubscribeError, PublishError, PushFrameError, + PushFrameErrorReason, RemoteDataTrackPipelineOptions, }, prelude::DataTrackSubscribeOptions, }; @@ -73,6 +74,55 @@ impl From for DataTrackSubscribeOptions { } } +impl From for DataTrackSchemaEncoding { + fn from(encoding: proto::DataTrackSchemaEncoding) -> Self { + match encoding { + proto::DataTrackSchemaEncoding::Protobuf => Self::Protobuf, + proto::DataTrackSchemaEncoding::Flatbuffer => Self::Flatbuffer, + proto::DataTrackSchemaEncoding::Ros1Msg => Self::Ros1Msg, + proto::DataTrackSchemaEncoding::Ros2Msg => Self::Ros2Msg, + proto::DataTrackSchemaEncoding::Ros2Idl => Self::Ros2Idl, + proto::DataTrackSchemaEncoding::OmgIdl => Self::OmgIdl, + proto::DataTrackSchemaEncoding::JsonSchema => Self::JsonSchema, + proto::DataTrackSchemaEncoding::Other => Self::Other, + } + } +} + +impl From for proto::DataTrackSchemaEncoding { + fn from(encoding: DataTrackSchemaEncoding) -> Self { + match encoding { + DataTrackSchemaEncoding::Protobuf => Self::Protobuf, + DataTrackSchemaEncoding::Flatbuffer => Self::Flatbuffer, + DataTrackSchemaEncoding::Ros1Msg => Self::Ros1Msg, + DataTrackSchemaEncoding::Ros2Msg => Self::Ros2Msg, + DataTrackSchemaEncoding::Ros2Idl => Self::Ros2Idl, + DataTrackSchemaEncoding::OmgIdl => Self::OmgIdl, + DataTrackSchemaEncoding::JsonSchema => Self::JsonSchema, + DataTrackSchemaEncoding::Other => Self::Other, + // `DataTrackSchemaEncoding` is `#[non_exhaustive]`; map any future + // variant to the catch-all encoding. + _ => Self::Other, + } + } +} + +impl From for DataTrackSchemaId { + fn from(msg: proto::DataTrackSchemaId) -> Self { + let encoding = msg.encoding().into(); + DataTrackSchemaId::new(msg.name, encoding) + } +} + +impl From for proto::DataTrackSchemaId { + fn from(id: DataTrackSchemaId) -> Self { + Self { + name: id.name().to_string(), + encoding: proto::DataTrackSchemaEncoding::from(id.encoding()) as i32, + } + } +} + impl From<&PublishError> for proto::PublishDataTrackErrorCode { fn from(err: &PublishError) -> Self { match err { diff --git a/livekit-ffi/src/server/participant.rs b/livekit-ffi/src/server/participant.rs index aff0bb6cc..0ed29b766 100644 --- a/livekit-ffi/src/server/participant.rs +++ b/livekit-ffi/src/server/participant.rs @@ -242,6 +242,48 @@ impl FfiParticipant { Ok(proto::TextStreamOpenResponse { async_id }) } + pub fn define_schema( + &self, + server: &'static FfiServer, + request: proto::DefineSchemaRequest, + ) -> FfiResult { + let async_id = server.resolve_async_id(request.request_async_id); + let local = self.guard_local_participant()?; + let schema_id = request.schema_id.into(); + + let handle = server.async_runtime.spawn(async move { + let res = local.define_schema(schema_id, request.definition).await; + let callback = + proto::DefineSchemaCallback { async_id, error: res.err().map(|e| e.to_string()) }; + let _ = server.send_event(callback.into()); + }); + server.watch_panic(handle); + Ok(proto::DefineSchemaResponse { async_id }) + } + + pub fn get_schema( + &self, + server: &'static FfiServer, + request: proto::GetSchemaRequest, + ) -> FfiResult { + let async_id = server.resolve_async_id(request.request_async_id); + let local = self.guard_local_participant()?; + let schema_id = request.schema_id.into(); + let participant = ParticipantIdentity::from(request.participant_identity); + + let handle = server.async_runtime.spawn(async move { + let result = local.get_schema(schema_id, participant).await; + let callback = proto::GetSchemaCallback { + async_id, + definition: result.as_ref().ok().cloned(), + error: result.as_ref().err().map(|e| e.to_string()), + }; + let _ = server.send_event(callback.into()); + }); + server.watch_panic(handle); + Ok(proto::GetSchemaResponse { async_id }) + } + pub fn publish_data_track( &self, server: &'static FfiServer, diff --git a/livekit-ffi/src/server/requests.rs b/livekit-ffi/src/server/requests.rs index 57c1e38d7..9b6aa5abe 100644 --- a/livekit-ffi/src/server/requests.rs +++ b/livekit-ffi/src/server/requests.rs @@ -1247,6 +1247,24 @@ fn on_publish_data_track( ffi_participant.publish_data_track(server, request) } +fn on_define_schema( + server: &'static FfiServer, + request: proto::DefineSchemaRequest, +) -> FfiResult { + let ffi_participant = + server.retrieve_handle::(request.local_participant_handle)?.clone(); + ffi_participant.define_schema(server, request) +} + +fn on_get_schema( + server: &'static FfiServer, + request: proto::GetSchemaRequest, +) -> FfiResult { + let ffi_participant = + server.retrieve_handle::(request.local_participant_handle)?.clone(); + ffi_participant.get_schema(server, request) +} + fn on_local_data_track_is_published( server: &'static FfiServer, request: proto::LocalDataTrackIsPublishedRequest, @@ -1424,6 +1442,8 @@ pub fn handle_request( on_remote_data_track_set_pipeline_options(server, req)?.into() } Request::DataTrackStreamRead(req) => on_data_track_stream_read(server, req)?.into(), + Request::DefineSchema(req) => on_define_schema(server, req)?.into(), + Request::GetSchema(req) => on_get_schema(server, req)?.into(), // Platform Audio Request::NewPlatformAudio(req) => { platform_audio::on_new_platform_audio(server, req)?.into() From df2d4f7a323725b77b2ee4d3facb8b7b5adc81f7 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:08:42 -0700 Subject: [PATCH 10/28] Changeset --- .changeset/data-track-schemas-ffi.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/data-track-schemas-ffi.md diff --git a/.changeset/data-track-schemas-ffi.md b/.changeset/data-track-schemas-ffi.md new file mode 100644 index 000000000..3127bafc3 --- /dev/null +++ b/.changeset/data-track-schemas-ffi.md @@ -0,0 +1,7 @@ +--- +livekit: patch +livekit-datatrack: patch +livekit-ffi: patch +--- + +Add schema metadata support for data tracks. From 2248728f6023323df6a9630ec139bfe43eca9fc4 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:18:12 -0700 Subject: [PATCH 11/28] Add E2E test cases --- livekit/tests/data_track_test.rs | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 6b7930deb..a61f726dc 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -439,6 +439,43 @@ async fn test_publisher_side_fault(scenario: SimulateScenario) -> Result<()> { Ok(()) } +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_schema_storage() -> Result<()> { + use livekit::data_track::{DataTrackSchemaEncoding, DataTrackSchemaId}; + + const DEFINITION: &str = "my schema definition"; + + let mut rooms = test_rooms(2).await?; + let (pub_room, _) = rooms.pop().unwrap(); + let (sub_room, _) = rooms.pop().unwrap(); + let pub_identity = pub_room.local_participant().identity(); + + let schema_id = DataTrackSchemaId::new("my_schema", DataTrackSchemaEncoding::JsonSchema); + + pub_room.local_participant().define_schema(schema_id.clone(), DEFINITION.to_string()).await?; + + let definition = sub_room.local_participant().get_schema(schema_id, pub_identity).await?; + assert_eq!(definition, DEFINITION); + + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_get_undefined_schema() -> Result<()> { + use livekit::data_track::{DataTrackSchemaEncoding, DataTrackSchemaId}; + + let (room, _) = test_rooms(1).await?.pop().unwrap(); + let identity = room.local_participant().identity(); + + let schema_id = DataTrackSchemaId::new("missing_schema", DataTrackSchemaEncoding::JsonSchema); + let result = room.local_participant().get_schema(schema_id, identity).await; + assert!(result.is_err()); + + Ok(()) +} + /// Waits for the first remote data track to be published. #[cfg(feature = "__lk-e2e-test")] async fn wait_for_remote_track( From 682a63cbb6564768053d8d8125f79f1b0da5f766 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:35:33 -0700 Subject: [PATCH 12/28] Document core types --- livekit-datatrack/src/schema.rs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/livekit-datatrack/src/schema.rs b/livekit-datatrack/src/schema.rs index f1e159426..ddf0874ef 100644 --- a/livekit-datatrack/src/schema.rs +++ b/livekit-datatrack/src/schema.rs @@ -17,13 +17,21 @@ use std::sync::Arc; /// Identifier for a data track schema. /// -/// A schema ID is a compound identifier consisting of two components: -/// - Name (e.g. "joint_positions") -/// - Encoding +/// A compound identifier with two components: name and encoding. /// -/// Two schema IDs with the same name but different encodings are not equivalent. +/// Two IDs are equal only if both components match; the same name with a +/// different encoding refers to a distinct schema. Cloning is cheap, as the name +/// component is reference counted. /// -/// Clones of this type are cheap since the name component is reference counted. +/// # Examples +/// +/// ``` +/// # use livekit_datatrack::api::{DataTrackSchemaId, DataTrackSchemaEncoding}; +/// let schema = DataTrackSchemaId::new("my_schema", DataTrackSchemaEncoding::Protobuf); +/// +/// assert_eq!(schema.name(), "my_schema"); +/// assert_eq!(schema.encoding(), DataTrackSchemaEncoding::Protobuf); +/// ``` /// #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct DataTrackSchemaId { @@ -50,7 +58,11 @@ impl DataTrackSchemaId { /// Encoding used for a schema definition. /// -/// See also: [`DataTrackSchemaId`] +/// Identifies the interface definition language the schema is written in (e.g. a +/// `.proto` file for [`Protobuf`]). This in turn dictates the wire format of the +/// frames the schema describes, captured by [`DataTrackFrameEncoding`]. +/// +/// [`Protobuf`]: DataTrackSchemaEncoding::Protobuf /// #[non_exhaustive] #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] @@ -89,6 +101,12 @@ pub enum DataTrackSchemaEncoding { } /// Encoding used for frames pushed on a data track. +/// +/// The serialization format of the frame bytes (e.g. [`Protobuf`]); the structure +/// of those bytes is described by a schema, see [`DataTrackSchemaEncoding`]. +/// +/// [`Protobuf`]: DataTrackFrameEncoding::Protobuf +/// #[non_exhaustive] #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] #[cfg_attr(test, derive(fake::Dummy))] From ea5c92ec4d77a8d5c83a543d2623eb975d93fa2c Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:57:21 -0700 Subject: [PATCH 13/28] Document store and get methods --- .../src/room/participant/local_participant.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 2613f0b0b..287d11089 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -918,11 +918,40 @@ impl LocalParticipant { // Local participants don't receive data messages, so this is a no-op } + /// Stores the definition of a data track schema. + /// + /// Called by a publisher to make a schema available to subscribers, who can + /// later look up its definition via [`get_schema`](Self::get_schema). Define a + /// schema before publishing any data track that references it, so that + /// subscribers can resolve the schema by its ID. + /// + /// A schema can only be defined once. Attempting to redefine an existing + /// schema returns an error. + /// + /// # Arguments + /// + /// * `id` - Identifies the schema; the same ID is provided when publishing a + /// data track that uses it. + /// * `definition` - The schema definition, stored as-is. It is neither parsed + /// nor validated against its [encoding](DataTrackSchemaId::encoding), so + /// the caller is responsible for ensuring it is well-formed. + /// pub async fn define_schema(&self, id: DataTrackSchemaId, definition: String) -> RoomResult<()> { self.inner.rtc_engine.store_data_blob(id.into(), definition.into()).await?; Ok(()) } + /// Retrieves the definition for a data track schema. + /// + /// Called by a subscriber that wants to inspect the schema a participant + /// [defined](Self::define_schema) for a data track it is publishing. Returns + /// an error if the participant has not defined a schema with this ID. + /// + /// # Arguments + /// + /// * `id` - Identifies the schema to retrieve. + /// * `participant` - Identity of the participant that defined the schema. + /// pub async fn get_schema( &self, id: DataTrackSchemaId, From b3e6fb7b7481f472d24460158ee4c4b5b7057192 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:58:31 +0000 Subject: [PATCH 14/28] generated protobuf --- .../proto/data_track_pb.d.ts | 283 ++++++++++++++++++ .../proto/data_track_pb.js | 113 +++++++ livekit-ffi-node-bindings/proto/ffi_pb.d.ts | 44 ++- livekit-ffi-node-bindings/proto/ffi_pb.js | 8 +- 4 files changed, 446 insertions(+), 2 deletions(-) diff --git a/livekit-ffi-node-bindings/proto/data_track_pb.d.ts b/livekit-ffi-node-bindings/proto/data_track_pb.d.ts index 7fe5fd23e..5f20ed358 100644 --- a/livekit-ffi-node-bindings/proto/data_track_pb.d.ts +++ b/livekit-ffi-node-bindings/proto/data_track_pb.d.ts @@ -196,6 +196,53 @@ export declare enum SubscribeDataTrackErrorCode { INTERNAL = 6, } +/** + * Encoding used to interpret a data track schema definition. + * + * @generated from enum livekit.proto.DataTrackSchemaEncoding + */ +export declare enum DataTrackSchemaEncoding { + /** + * @generated from enum value: DATA_TRACK_SCHEMA_ENCODING_PROTOBUF = 0; + */ + PROTOBUF = 0, + + /** + * @generated from enum value: DATA_TRACK_SCHEMA_ENCODING_FLATBUFFER = 1; + */ + FLATBUFFER = 1, + + /** + * @generated from enum value: DATA_TRACK_SCHEMA_ENCODING_ROS1_MSG = 2; + */ + ROS1_MSG = 2, + + /** + * @generated from enum value: DATA_TRACK_SCHEMA_ENCODING_ROS2_MSG = 3; + */ + ROS2_MSG = 3, + + /** + * @generated from enum value: DATA_TRACK_SCHEMA_ENCODING_ROS2_IDL = 4; + */ + ROS2_IDL = 4, + + /** + * @generated from enum value: DATA_TRACK_SCHEMA_ENCODING_OMG_IDL = 5; + */ + OMG_IDL = 5, + + /** + * @generated from enum value: DATA_TRACK_SCHEMA_ENCODING_JSON_SCHEMA = 6; + */ + JSON_SCHEMA = 6, + + /** + * @generated from enum value: DATA_TRACK_SCHEMA_ENCODING_OTHER = 7; + */ + OTHER = 7, +} + /** * Information about a published data track. * @@ -1128,3 +1175,239 @@ export declare class DataTrackStreamEOS extends Message { static equals(a: DataTrackStreamEOS | PlainMessage | undefined, b: DataTrackStreamEOS | PlainMessage | undefined): boolean; } +/** + * Uniquely identifies a data track schema. + * + * @generated from message livekit.proto.DataTrackSchemaId + */ +export declare class DataTrackSchemaId extends Message { + /** + * Name component of the schema identifier. + * + * @generated from field: required string name = 1; + */ + name?: string; + + /** + * Encoding of the schema definition. + * + * @generated from field: required livekit.proto.DataTrackSchemaEncoding encoding = 2; + */ + encoding?: DataTrackSchemaEncoding; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto2; + static readonly typeName = "livekit.proto.DataTrackSchemaId"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): DataTrackSchemaId; + + static fromJson(jsonValue: JsonValue, options?: Partial): DataTrackSchemaId; + + static fromJsonString(jsonString: string, options?: Partial): DataTrackSchemaId; + + static equals(a: DataTrackSchemaId | PlainMessage | undefined, b: DataTrackSchemaId | PlainMessage | undefined): boolean; +} + +/** + * Define (store) a schema definition for the local participant. + * + * @generated from message livekit.proto.DefineSchemaRequest + */ +export declare class DefineSchemaRequest extends Message { + /** + * @generated from field: required uint64 local_participant_handle = 1; + */ + localParticipantHandle?: bigint; + + /** + * @generated from field: required livekit.proto.DataTrackSchemaId schema_id = 2; + */ + schemaId?: DataTrackSchemaId; + + /** + * @generated from field: required string definition = 3; + */ + definition?: string; + + /** + * @generated from field: optional uint64 request_async_id = 4; + */ + requestAsyncId?: bigint; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto2; + static readonly typeName = "livekit.proto.DefineSchemaRequest"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): DefineSchemaRequest; + + static fromJson(jsonValue: JsonValue, options?: Partial): DefineSchemaRequest; + + static fromJsonString(jsonString: string, options?: Partial): DefineSchemaRequest; + + static equals(a: DefineSchemaRequest | PlainMessage | undefined, b: DefineSchemaRequest | PlainMessage | undefined): boolean; +} + +/** + * @generated from message livekit.proto.DefineSchemaResponse + */ +export declare class DefineSchemaResponse extends Message { + /** + * @generated from field: required uint64 async_id = 1; + */ + asyncId?: bigint; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto2; + static readonly typeName = "livekit.proto.DefineSchemaResponse"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): DefineSchemaResponse; + + static fromJson(jsonValue: JsonValue, options?: Partial): DefineSchemaResponse; + + static fromJsonString(jsonString: string, options?: Partial): DefineSchemaResponse; + + static equals(a: DefineSchemaResponse | PlainMessage | undefined, b: DefineSchemaResponse | PlainMessage | undefined): boolean; +} + +/** + * @generated from message livekit.proto.DefineSchemaCallback + */ +export declare class DefineSchemaCallback extends Message { + /** + * @generated from field: required uint64 async_id = 1; + */ + asyncId?: bigint; + + /** + * Present if the schema could not be defined. + * + * @generated from field: optional string error = 2; + */ + error?: string; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto2; + static readonly typeName = "livekit.proto.DefineSchemaCallback"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): DefineSchemaCallback; + + static fromJson(jsonValue: JsonValue, options?: Partial): DefineSchemaCallback; + + static fromJsonString(jsonString: string, options?: Partial): DefineSchemaCallback; + + static equals(a: DefineSchemaCallback | PlainMessage | undefined, b: DefineSchemaCallback | PlainMessage | undefined): boolean; +} + +/** + * Retrieve a schema definition previously stored by a participant. + * + * @generated from message livekit.proto.GetSchemaRequest + */ +export declare class GetSchemaRequest extends Message { + /** + * @generated from field: required uint64 local_participant_handle = 1; + */ + localParticipantHandle?: bigint; + + /** + * @generated from field: required livekit.proto.DataTrackSchemaId schema_id = 2; + */ + schemaId?: DataTrackSchemaId; + + /** + * Identity of the participant who owns the schema. + * + * @generated from field: required string participant_identity = 3; + */ + participantIdentity?: string; + + /** + * @generated from field: optional uint64 request_async_id = 4; + */ + requestAsyncId?: bigint; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto2; + static readonly typeName = "livekit.proto.GetSchemaRequest"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): GetSchemaRequest; + + static fromJson(jsonValue: JsonValue, options?: Partial): GetSchemaRequest; + + static fromJsonString(jsonString: string, options?: Partial): GetSchemaRequest; + + static equals(a: GetSchemaRequest | PlainMessage | undefined, b: GetSchemaRequest | PlainMessage | undefined): boolean; +} + +/** + * @generated from message livekit.proto.GetSchemaResponse + */ +export declare class GetSchemaResponse extends Message { + /** + * @generated from field: required uint64 async_id = 1; + */ + asyncId?: bigint; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto2; + static readonly typeName = "livekit.proto.GetSchemaResponse"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): GetSchemaResponse; + + static fromJson(jsonValue: JsonValue, options?: Partial): GetSchemaResponse; + + static fromJsonString(jsonString: string, options?: Partial): GetSchemaResponse; + + static equals(a: GetSchemaResponse | PlainMessage | undefined, b: GetSchemaResponse | PlainMessage | undefined): boolean; +} + +/** + * @generated from message livekit.proto.GetSchemaCallback + */ +export declare class GetSchemaCallback extends Message { + /** + * @generated from field: required uint64 async_id = 1; + */ + asyncId?: bigint; + + /** + * The schema definition, present on success. + * + * @generated from field: optional string definition = 2; + */ + definition?: string; + + /** + * Present if the schema could not be retrieved. + * + * @generated from field: optional string error = 3; + */ + error?: string; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto2; + static readonly typeName = "livekit.proto.GetSchemaCallback"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): GetSchemaCallback; + + static fromJson(jsonValue: JsonValue, options?: Partial): GetSchemaCallback; + + static fromJsonString(jsonString: string, options?: Partial): GetSchemaCallback; + + static equals(a: GetSchemaCallback | PlainMessage | undefined, b: GetSchemaCallback | PlainMessage | undefined): boolean; +} + diff --git a/livekit-ffi-node-bindings/proto/data_track_pb.js b/livekit-ffi-node-bindings/proto/data_track_pb.js index 73b496d21..d5f327613 100644 --- a/livekit-ffi-node-bindings/proto/data_track_pb.js +++ b/livekit-ffi-node-bindings/proto/data_track_pb.js @@ -90,6 +90,25 @@ const SubscribeDataTrackErrorCode = /*@__PURE__*/ proto2.makeEnum( ], ); +/** + * Encoding used to interpret a data track schema definition. + * + * @generated from enum livekit.proto.DataTrackSchemaEncoding + */ +const DataTrackSchemaEncoding = /*@__PURE__*/ proto2.makeEnum( + "livekit.proto.DataTrackSchemaEncoding", + [ + {no: 0, name: "DATA_TRACK_SCHEMA_ENCODING_PROTOBUF", localName: "PROTOBUF"}, + {no: 1, name: "DATA_TRACK_SCHEMA_ENCODING_FLATBUFFER", localName: "FLATBUFFER"}, + {no: 2, name: "DATA_TRACK_SCHEMA_ENCODING_ROS1_MSG", localName: "ROS1_MSG"}, + {no: 3, name: "DATA_TRACK_SCHEMA_ENCODING_ROS2_MSG", localName: "ROS2_MSG"}, + {no: 4, name: "DATA_TRACK_SCHEMA_ENCODING_ROS2_IDL", localName: "ROS2_IDL"}, + {no: 5, name: "DATA_TRACK_SCHEMA_ENCODING_OMG_IDL", localName: "OMG_IDL"}, + {no: 6, name: "DATA_TRACK_SCHEMA_ENCODING_JSON_SCHEMA", localName: "JSON_SCHEMA"}, + {no: 7, name: "DATA_TRACK_SCHEMA_ENCODING_OTHER", localName: "OTHER"}, + ], +); + /** * Information about a published data track. * @@ -465,11 +484,98 @@ const DataTrackStreamEOS = /*@__PURE__*/ proto2.makeMessageType( ], ); +/** + * Uniquely identifies a data track schema. + * + * @generated from message livekit.proto.DataTrackSchemaId + */ +const DataTrackSchemaId = /*@__PURE__*/ proto2.makeMessageType( + "livekit.proto.DataTrackSchemaId", + () => [ + { no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */, req: true }, + { no: 2, name: "encoding", kind: "enum", T: proto2.getEnumType(DataTrackSchemaEncoding), req: true }, + ], +); + +/** + * Define (store) a schema definition for the local participant. + * + * @generated from message livekit.proto.DefineSchemaRequest + */ +const DefineSchemaRequest = /*@__PURE__*/ proto2.makeMessageType( + "livekit.proto.DefineSchemaRequest", + () => [ + { no: 1, name: "local_participant_handle", kind: "scalar", T: 4 /* ScalarType.UINT64 */, req: true }, + { no: 2, name: "schema_id", kind: "message", T: DataTrackSchemaId, req: true }, + { no: 3, name: "definition", kind: "scalar", T: 9 /* ScalarType.STRING */, req: true }, + { no: 4, name: "request_async_id", kind: "scalar", T: 4 /* ScalarType.UINT64 */, opt: true }, + ], +); + +/** + * @generated from message livekit.proto.DefineSchemaResponse + */ +const DefineSchemaResponse = /*@__PURE__*/ proto2.makeMessageType( + "livekit.proto.DefineSchemaResponse", + () => [ + { no: 1, name: "async_id", kind: "scalar", T: 4 /* ScalarType.UINT64 */, req: true }, + ], +); + +/** + * @generated from message livekit.proto.DefineSchemaCallback + */ +const DefineSchemaCallback = /*@__PURE__*/ proto2.makeMessageType( + "livekit.proto.DefineSchemaCallback", + () => [ + { no: 1, name: "async_id", kind: "scalar", T: 4 /* ScalarType.UINT64 */, req: true }, + { no: 2, name: "error", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, + ], +); + +/** + * Retrieve a schema definition previously stored by a participant. + * + * @generated from message livekit.proto.GetSchemaRequest + */ +const GetSchemaRequest = /*@__PURE__*/ proto2.makeMessageType( + "livekit.proto.GetSchemaRequest", + () => [ + { no: 1, name: "local_participant_handle", kind: "scalar", T: 4 /* ScalarType.UINT64 */, req: true }, + { no: 2, name: "schema_id", kind: "message", T: DataTrackSchemaId, req: true }, + { no: 3, name: "participant_identity", kind: "scalar", T: 9 /* ScalarType.STRING */, req: true }, + { no: 4, name: "request_async_id", kind: "scalar", T: 4 /* ScalarType.UINT64 */, opt: true }, + ], +); + +/** + * @generated from message livekit.proto.GetSchemaResponse + */ +const GetSchemaResponse = /*@__PURE__*/ proto2.makeMessageType( + "livekit.proto.GetSchemaResponse", + () => [ + { no: 1, name: "async_id", kind: "scalar", T: 4 /* ScalarType.UINT64 */, req: true }, + ], +); + +/** + * @generated from message livekit.proto.GetSchemaCallback + */ +const GetSchemaCallback = /*@__PURE__*/ proto2.makeMessageType( + "livekit.proto.GetSchemaCallback", + () => [ + { no: 1, name: "async_id", kind: "scalar", T: 4 /* ScalarType.UINT64 */, req: true }, + { no: 2, name: "definition", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, + { no: 3, name: "error", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, + ], +); + exports.DataTrackErrorCode = DataTrackErrorCode; exports.PublishDataTrackErrorCode = PublishDataTrackErrorCode; exports.LocalDataTrackTryPushErrorCode = LocalDataTrackTryPushErrorCode; exports.SubscribeDataTrackErrorCode = SubscribeDataTrackErrorCode; +exports.DataTrackSchemaEncoding = DataTrackSchemaEncoding; exports.DataTrackInfo = DataTrackInfo; exports.DataTrackFrame = DataTrackFrame; exports.DataTrackError = DataTrackError; @@ -502,3 +608,10 @@ exports.DataTrackStreamReadResponse = DataTrackStreamReadResponse; exports.DataTrackStreamEvent = DataTrackStreamEvent; exports.DataTrackStreamFrameReceived = DataTrackStreamFrameReceived; exports.DataTrackStreamEOS = DataTrackStreamEOS; +exports.DataTrackSchemaId = DataTrackSchemaId; +exports.DefineSchemaRequest = DefineSchemaRequest; +exports.DefineSchemaResponse = DefineSchemaResponse; +exports.DefineSchemaCallback = DefineSchemaCallback; +exports.GetSchemaRequest = GetSchemaRequest; +exports.GetSchemaResponse = GetSchemaResponse; +exports.GetSchemaCallback = GetSchemaCallback; diff --git a/livekit-ffi-node-bindings/proto/ffi_pb.d.ts b/livekit-ffi-node-bindings/proto/ffi_pb.d.ts index 930dd7245..ac81cb93e 100644 --- a/livekit-ffi-node-bindings/proto/ffi_pb.d.ts +++ b/livekit-ffi-node-bindings/proto/ffi_pb.d.ts @@ -27,7 +27,7 @@ import type { E2eeRequest, E2eeResponse } from "./e2ee_pb.js"; import type { PerformRpcCallback, PerformRpcRequest, PerformRpcResponse, RegisterRpcMethodRequest, RegisterRpcMethodResponse, RpcMethodInvocationEvent, RpcMethodInvocationResponseRequest, RpcMethodInvocationResponseResponse, UnregisterRpcMethodRequest, UnregisterRpcMethodResponse } from "./rpc_pb.js"; import type { EnableRemoteTrackPublicationRequest, EnableRemoteTrackPublicationResponse, SetRemoteTrackPublicationQualityRequest, SetRemoteTrackPublicationQualityResponse, UpdateRemoteTrackPublicationDimensionRequest, UpdateRemoteTrackPublicationDimensionResponse } from "./track_publication_pb.js"; import type { ByteStreamOpenCallback, ByteStreamOpenRequest, ByteStreamOpenResponse, ByteStreamReaderEvent, ByteStreamReaderReadAllCallback, ByteStreamReaderReadAllRequest, ByteStreamReaderReadAllResponse, ByteStreamReaderReadIncrementalRequest, ByteStreamReaderReadIncrementalResponse, ByteStreamReaderWriteToFileCallback, ByteStreamReaderWriteToFileRequest, ByteStreamReaderWriteToFileResponse, ByteStreamWriterCloseCallback, ByteStreamWriterCloseRequest, ByteStreamWriterCloseResponse, ByteStreamWriterWriteCallback, ByteStreamWriterWriteRequest, ByteStreamWriterWriteResponse, StreamSendBytesCallback, StreamSendBytesRequest, StreamSendBytesResponse, StreamSendFileCallback, StreamSendFileRequest, StreamSendFileResponse, StreamSendTextCallback, StreamSendTextRequest, StreamSendTextResponse, TextStreamOpenCallback, TextStreamOpenRequest, TextStreamOpenResponse, TextStreamReaderEvent, TextStreamReaderReadAllCallback, TextStreamReaderReadAllRequest, TextStreamReaderReadAllResponse, TextStreamReaderReadIncrementalRequest, TextStreamReaderReadIncrementalResponse, TextStreamWriterCloseCallback, TextStreamWriterCloseRequest, TextStreamWriterCloseResponse, TextStreamWriterWriteCallback, TextStreamWriterWriteRequest, TextStreamWriterWriteResponse } from "./data_stream_pb.js"; -import type { DataTrackStreamEvent, DataTrackStreamReadRequest, DataTrackStreamReadResponse, LocalDataTrackIsPublishedRequest, LocalDataTrackIsPublishedResponse, LocalDataTrackTryPushRequest, LocalDataTrackTryPushResponse, LocalDataTrackUnpublishRequest, LocalDataTrackUnpublishResponse, PublishDataTrackCallback, PublishDataTrackRequest, PublishDataTrackResponse, RemoteDataTrackIsPublishedRequest, RemoteDataTrackIsPublishedResponse, RemoteDataTrackSetPipelineOptionsRequest, RemoteDataTrackSetPipelineOptionsResponse, SubscribeDataTrackRequest, SubscribeDataTrackResponse } from "./data_track_pb.js"; +import type { DataTrackStreamEvent, DataTrackStreamReadRequest, DataTrackStreamReadResponse, DefineSchemaCallback, DefineSchemaRequest, DefineSchemaResponse, GetSchemaCallback, GetSchemaRequest, GetSchemaResponse, LocalDataTrackIsPublishedRequest, LocalDataTrackIsPublishedResponse, LocalDataTrackTryPushRequest, LocalDataTrackTryPushResponse, LocalDataTrackUnpublishRequest, LocalDataTrackUnpublishResponse, PublishDataTrackCallback, PublishDataTrackRequest, PublishDataTrackResponse, RemoteDataTrackIsPublishedRequest, RemoteDataTrackIsPublishedResponse, RemoteDataTrackSetPipelineOptionsRequest, RemoteDataTrackSetPipelineOptionsResponse, SubscribeDataTrackRequest, SubscribeDataTrackResponse } from "./data_track_pb.js"; /** * @generated from enum livekit.proto.LogLevel @@ -543,6 +543,20 @@ export declare class FfiRequest extends Message { */ value: RemoteDataTrackSetPipelineOptionsRequest; case: "remoteDataTrackSetPipelineOptions"; + } | { + /** + * Data Track (schemas) + * + * @generated from field: livekit.proto.DefineSchemaRequest define_schema = 85; + */ + value: DefineSchemaRequest; + case: "defineSchema"; + } | { + /** + * @generated from field: livekit.proto.GetSchemaRequest get_schema = 86; + */ + value: GetSchemaRequest; + case: "getSchema"; } | { /** * Reconnection / chaos testing @@ -1091,6 +1105,20 @@ export declare class FfiResponse extends Message { */ value: RemoteDataTrackSetPipelineOptionsResponse; case: "remoteDataTrackSetPipelineOptions"; + } | { + /** + * Data Track (schemas) + * + * @generated from field: livekit.proto.DefineSchemaResponse define_schema = 85; + */ + value: DefineSchemaResponse; + case: "defineSchema"; + } | { + /** + * @generated from field: livekit.proto.GetSchemaResponse get_schema = 86; + */ + value: GetSchemaResponse; + case: "getSchema"; } | { /** * Reconnection / chaos testing @@ -1439,6 +1467,20 @@ export declare class FfiEvent extends Message { */ value: SimulateScenarioCallback; case: "simulateScenario"; + } | { + /** + * Data Track (schemas) + * + * @generated from field: livekit.proto.DefineSchemaCallback define_schema = 45; + */ + value: DefineSchemaCallback; + case: "defineSchema"; + } | { + /** + * @generated from field: livekit.proto.GetSchemaCallback get_schema = 46; + */ + value: GetSchemaCallback; + case: "getSchema"; } | { case: undefined; value?: undefined }; constructor(data?: PartialMessage); diff --git a/livekit-ffi-node-bindings/proto/ffi_pb.js b/livekit-ffi-node-bindings/proto/ffi_pb.js index 10986d29d..4f0bdd422 100644 --- a/livekit-ffi-node-bindings/proto/ffi_pb.js +++ b/livekit-ffi-node-bindings/proto/ffi_pb.js @@ -29,7 +29,7 @@ const { E2eeRequest, E2eeResponse } = require("./e2ee_pb.js"); const { PerformRpcCallback, PerformRpcRequest, PerformRpcResponse, RegisterRpcMethodRequest, RegisterRpcMethodResponse, RpcMethodInvocationEvent, RpcMethodInvocationResponseRequest, RpcMethodInvocationResponseResponse, UnregisterRpcMethodRequest, UnregisterRpcMethodResponse } = require("./rpc_pb.js"); const { EnableRemoteTrackPublicationRequest, EnableRemoteTrackPublicationResponse, SetRemoteTrackPublicationQualityRequest, SetRemoteTrackPublicationQualityResponse, UpdateRemoteTrackPublicationDimensionRequest, UpdateRemoteTrackPublicationDimensionResponse } = require("./track_publication_pb.js"); const { ByteStreamOpenCallback, ByteStreamOpenRequest, ByteStreamOpenResponse, ByteStreamReaderEvent, ByteStreamReaderReadAllCallback, ByteStreamReaderReadAllRequest, ByteStreamReaderReadAllResponse, ByteStreamReaderReadIncrementalRequest, ByteStreamReaderReadIncrementalResponse, ByteStreamReaderWriteToFileCallback, ByteStreamReaderWriteToFileRequest, ByteStreamReaderWriteToFileResponse, ByteStreamWriterCloseCallback, ByteStreamWriterCloseRequest, ByteStreamWriterCloseResponse, ByteStreamWriterWriteCallback, ByteStreamWriterWriteRequest, ByteStreamWriterWriteResponse, StreamSendBytesCallback, StreamSendBytesRequest, StreamSendBytesResponse, StreamSendFileCallback, StreamSendFileRequest, StreamSendFileResponse, StreamSendTextCallback, StreamSendTextRequest, StreamSendTextResponse, TextStreamOpenCallback, TextStreamOpenRequest, TextStreamOpenResponse, TextStreamReaderEvent, TextStreamReaderReadAllCallback, TextStreamReaderReadAllRequest, TextStreamReaderReadAllResponse, TextStreamReaderReadIncrementalRequest, TextStreamReaderReadIncrementalResponse, TextStreamWriterCloseCallback, TextStreamWriterCloseRequest, TextStreamWriterCloseResponse, TextStreamWriterWriteCallback, TextStreamWriterWriteRequest, TextStreamWriterWriteResponse } = require("./data_stream_pb.js"); -const { DataTrackStreamEvent, DataTrackStreamReadRequest, DataTrackStreamReadResponse, LocalDataTrackIsPublishedRequest, LocalDataTrackIsPublishedResponse, LocalDataTrackTryPushRequest, LocalDataTrackTryPushResponse, LocalDataTrackUnpublishRequest, LocalDataTrackUnpublishResponse, PublishDataTrackCallback, PublishDataTrackRequest, PublishDataTrackResponse, RemoteDataTrackIsPublishedRequest, RemoteDataTrackIsPublishedResponse, RemoteDataTrackSetPipelineOptionsRequest, RemoteDataTrackSetPipelineOptionsResponse, SubscribeDataTrackRequest, SubscribeDataTrackResponse } = require("./data_track_pb.js"); +const { DataTrackStreamEvent, DataTrackStreamReadRequest, DataTrackStreamReadResponse, DefineSchemaCallback, DefineSchemaRequest, DefineSchemaResponse, GetSchemaCallback, GetSchemaRequest, GetSchemaResponse, LocalDataTrackIsPublishedRequest, LocalDataTrackIsPublishedResponse, LocalDataTrackTryPushRequest, LocalDataTrackTryPushResponse, LocalDataTrackUnpublishRequest, LocalDataTrackUnpublishResponse, PublishDataTrackCallback, PublishDataTrackRequest, PublishDataTrackResponse, RemoteDataTrackIsPublishedRequest, RemoteDataTrackIsPublishedResponse, RemoteDataTrackSetPipelineOptionsRequest, RemoteDataTrackSetPipelineOptionsResponse, SubscribeDataTrackRequest, SubscribeDataTrackResponse } = require("./data_track_pb.js"); /** * @generated from enum livekit.proto.LogLevel @@ -129,6 +129,8 @@ const FfiRequest = /*@__PURE__*/ proto2.makeMessageType( { no: 74, name: "remote_data_track_is_published", kind: "message", T: RemoteDataTrackIsPublishedRequest, oneof: "message" }, { no: 75, name: "data_track_stream_read", kind: "message", T: DataTrackStreamReadRequest, oneof: "message" }, { no: 84, name: "remote_data_track_set_pipeline_options", kind: "message", T: RemoteDataTrackSetPipelineOptionsRequest, oneof: "message" }, + { no: 85, name: "define_schema", kind: "message", T: DefineSchemaRequest, oneof: "message" }, + { no: 86, name: "get_schema", kind: "message", T: GetSchemaRequest, oneof: "message" }, { no: 76, name: "simulate_scenario", kind: "message", T: SimulateScenarioRequest, oneof: "message" }, { no: 77, name: "new_platform_audio", kind: "message", T: NewPlatformAudioRequest, oneof: "message" }, { no: 78, name: "get_audio_devices", kind: "message", T: GetAudioDevicesRequest, oneof: "message" }, @@ -222,6 +224,8 @@ const FfiResponse = /*@__PURE__*/ proto2.makeMessageType( { no: 73, name: "remote_data_track_is_published", kind: "message", T: RemoteDataTrackIsPublishedResponse, oneof: "message" }, { no: 74, name: "data_track_stream_read", kind: "message", T: DataTrackStreamReadResponse, oneof: "message" }, { no: 84, name: "remote_data_track_set_pipeline_options", kind: "message", T: RemoteDataTrackSetPipelineOptionsResponse, oneof: "message" }, + { no: 85, name: "define_schema", kind: "message", T: DefineSchemaResponse, oneof: "message" }, + { no: 86, name: "get_schema", kind: "message", T: GetSchemaResponse, oneof: "message" }, { no: 75, name: "simulate_scenario", kind: "message", T: SimulateScenarioResponse, oneof: "message" }, { no: 76, name: "new_platform_audio", kind: "message", T: NewPlatformAudioResponse, oneof: "message" }, { no: 77, name: "get_audio_devices", kind: "message", T: GetAudioDevicesResponse, oneof: "message" }, @@ -286,6 +290,8 @@ const FfiEvent = /*@__PURE__*/ proto2.makeMessageType( { no: 42, name: "publish_data_track", kind: "message", T: PublishDataTrackCallback, oneof: "message" }, { no: 43, name: "data_track_stream_event", kind: "message", T: DataTrackStreamEvent, oneof: "message" }, { no: 44, name: "simulate_scenario", kind: "message", T: SimulateScenarioCallback, oneof: "message" }, + { no: 45, name: "define_schema", kind: "message", T: DefineSchemaCallback, oneof: "message" }, + { no: 46, name: "get_schema", kind: "message", T: GetSchemaCallback, oneof: "message" }, ], ); From 8f74a90151c149b59b19ee3e8f2d7f583d37d56e Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:40:58 -0700 Subject: [PATCH 15/28] Update protocol Adds request ID fields --- livekit-protocol/protocol | 2 +- livekit-protocol/src/livekit.rs | 12 ++-- livekit-protocol/src/livekit.serde.rs | 88 ++++++++++++++++++--------- 3 files changed, 68 insertions(+), 34 deletions(-) diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index dfa6d45c5..41c46d1fa 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit dfa6d45c5cc4e457e8a58a7022f9ac8098cb63c6 +Subproject commit 41c46d1fa667e45c606741e97b69ba6fddf2f2da diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index e36d4539b..81b10bb1d 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -4107,6 +4107,8 @@ pub mod update_data_subscription { pub struct StoreDataBlobRequest { #[prost(message, optional, tag="1")] pub blob: ::core::option::Option, + #[prost(uint32, tag="2")] + pub request_id: u32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -4117,12 +4119,16 @@ pub struct GetDataBlobRequest { /// Unique key of the data blob to retrieve. #[prost(message, optional, tag="2")] pub key: ::core::option::Option, + #[prost(uint32, tag="3")] + pub request_id: u32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetDataBlobResponse { #[prost(message, optional, tag="1")] pub blob: ::core::option::Option, + #[prost(uint32, tag="2")] + pub request_id: u32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -4515,7 +4521,7 @@ pub struct RequestResponse { pub reason: i32, #[prost(string, tag="3")] pub message: ::prost::alloc::string::String, - #[prost(oneof="request_response::Request", tags="4, 5, 6, 7, 8, 9, 10, 11, 12, 13")] + #[prost(oneof="request_response::Request", tags="4, 5, 6, 7, 8, 9, 10, 11")] pub request: ::core::option::Option, } /// Nested message and enum types in `RequestResponse`. @@ -4595,10 +4601,6 @@ pub mod request_response { PublishDataTrack(super::PublishDataTrackRequest), #[prost(message, tag="11")] UnpublishDataTrack(super::UnpublishDataTrackRequest), - #[prost(message, tag="12")] - StoreDataBlob(super::StoreDataBlobRequest), - #[prost(message, tag="13")] - GetDataBlob(super::GetDataBlobRequest), } } #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index 1456cf503..e2e040b48 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -18910,6 +18910,9 @@ impl serde::Serialize for GetDataBlobRequest { if self.key.is_some() { len += 1; } + if self.request_id != 0 { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.GetDataBlobRequest", len)?; if !self.participant_identity.is_empty() { struct_ser.serialize_field("participantIdentity", &self.participant_identity)?; @@ -18917,6 +18920,9 @@ impl serde::Serialize for GetDataBlobRequest { if let Some(v) = self.key.as_ref() { struct_ser.serialize_field("key", v)?; } + if self.request_id != 0 { + struct_ser.serialize_field("requestId", &self.request_id)?; + } struct_ser.end() } } @@ -18930,12 +18936,15 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobRequest { "participant_identity", "participantIdentity", "key", + "request_id", + "requestId", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { ParticipantIdentity, Key, + RequestId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -18960,6 +18969,7 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobRequest { match value { "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), "key" => Ok(GeneratedField::Key), + "requestId" | "request_id" => Ok(GeneratedField::RequestId), _ => Ok(GeneratedField::__SkipField__), } } @@ -18981,6 +18991,7 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobRequest { { let mut participant_identity__ = None; let mut key__ = None; + let mut request_id__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::ParticipantIdentity => { @@ -18995,6 +19006,14 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobRequest { } key__ = map_.next_value()?; } + GeneratedField::RequestId => { + if request_id__.is_some() { + return Err(serde::de::Error::duplicate_field("requestId")); + } + request_id__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -19003,6 +19022,7 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobRequest { Ok(GetDataBlobRequest { participant_identity: participant_identity__.unwrap_or_default(), key: key__, + request_id: request_id__.unwrap_or_default(), }) } } @@ -19020,10 +19040,16 @@ impl serde::Serialize for GetDataBlobResponse { if self.blob.is_some() { len += 1; } + if self.request_id != 0 { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.GetDataBlobResponse", len)?; if let Some(v) = self.blob.as_ref() { struct_ser.serialize_field("blob", v)?; } + if self.request_id != 0 { + struct_ser.serialize_field("requestId", &self.request_id)?; + } struct_ser.end() } } @@ -19035,11 +19061,14 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobResponse { { const FIELDS: &[&str] = &[ "blob", + "request_id", + "requestId", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { Blob, + RequestId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -19063,6 +19092,7 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobResponse { { match value { "blob" => Ok(GeneratedField::Blob), + "requestId" | "request_id" => Ok(GeneratedField::RequestId), _ => Ok(GeneratedField::__SkipField__), } } @@ -19083,6 +19113,7 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobResponse { V: serde::de::MapAccess<'de>, { let mut blob__ = None; + let mut request_id__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Blob => { @@ -19091,6 +19122,14 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobResponse { } blob__ = map_.next_value()?; } + GeneratedField::RequestId => { + if request_id__.is_some() { + return Err(serde::de::Error::duplicate_field("requestId")); + } + request_id__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -19098,6 +19137,7 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobResponse { } Ok(GetDataBlobResponse { blob: blob__, + request_id: request_id__.unwrap_or_default(), }) } } @@ -33494,12 +33534,6 @@ impl serde::Serialize for RequestResponse { request_response::Request::UnpublishDataTrack(v) => { struct_ser.serialize_field("unpublishDataTrack", v)?; } - request_response::Request::StoreDataBlob(v) => { - struct_ser.serialize_field("storeDataBlob", v)?; - } - request_response::Request::GetDataBlob(v) => { - struct_ser.serialize_field("getDataBlob", v)?; - } } } struct_ser.end() @@ -33530,10 +33564,6 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "publishDataTrack", "unpublish_data_track", "unpublishDataTrack", - "store_data_blob", - "storeDataBlob", - "get_data_blob", - "getDataBlob", ]; #[allow(clippy::enum_variant_names)] @@ -33549,8 +33579,6 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { UpdateVideoTrack, PublishDataTrack, UnpublishDataTrack, - StoreDataBlob, - GetDataBlob, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -33584,8 +33612,6 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "updateVideoTrack" | "update_video_track" => Ok(GeneratedField::UpdateVideoTrack), "publishDataTrack" | "publish_data_track" => Ok(GeneratedField::PublishDataTrack), "unpublishDataTrack" | "unpublish_data_track" => Ok(GeneratedField::UnpublishDataTrack), - "storeDataBlob" | "store_data_blob" => Ok(GeneratedField::StoreDataBlob), - "getDataBlob" | "get_data_blob" => Ok(GeneratedField::GetDataBlob), _ => Ok(GeneratedField::__SkipField__), } } @@ -33685,20 +33711,6 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { return Err(serde::de::Error::duplicate_field("unpublishDataTrack")); } request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UnpublishDataTrack) -; - } - GeneratedField::StoreDataBlob => { - if request__.is_some() { - return Err(serde::de::Error::duplicate_field("storeDataBlob")); - } - request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::StoreDataBlob) -; - } - GeneratedField::GetDataBlob => { - if request__.is_some() { - return Err(serde::de::Error::duplicate_field("getDataBlob")); - } - request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::GetDataBlob) ; } GeneratedField::__SkipField__ => { @@ -46000,10 +46012,16 @@ impl serde::Serialize for StoreDataBlobRequest { if self.blob.is_some() { len += 1; } + if self.request_id != 0 { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.StoreDataBlobRequest", len)?; if let Some(v) = self.blob.as_ref() { struct_ser.serialize_field("blob", v)?; } + if self.request_id != 0 { + struct_ser.serialize_field("requestId", &self.request_id)?; + } struct_ser.end() } } @@ -46015,11 +46033,14 @@ impl<'de> serde::Deserialize<'de> for StoreDataBlobRequest { { const FIELDS: &[&str] = &[ "blob", + "request_id", + "requestId", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { Blob, + RequestId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -46043,6 +46064,7 @@ impl<'de> serde::Deserialize<'de> for StoreDataBlobRequest { { match value { "blob" => Ok(GeneratedField::Blob), + "requestId" | "request_id" => Ok(GeneratedField::RequestId), _ => Ok(GeneratedField::__SkipField__), } } @@ -46063,6 +46085,7 @@ impl<'de> serde::Deserialize<'de> for StoreDataBlobRequest { V: serde::de::MapAccess<'de>, { let mut blob__ = None; + let mut request_id__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Blob => { @@ -46071,6 +46094,14 @@ impl<'de> serde::Deserialize<'de> for StoreDataBlobRequest { } blob__ = map_.next_value()?; } + GeneratedField::RequestId => { + if request_id__.is_some() { + return Err(serde::de::Error::duplicate_field("requestId")); + } + request_id__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -46078,6 +46109,7 @@ impl<'de> serde::Deserialize<'de> for StoreDataBlobRequest { } Ok(StoreDataBlobRequest { blob: blob__, + request_id: request_id__.unwrap_or_default(), }) } } From 49b4fd9c55a3627efec81fd881b29fccfb37157e Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:14:06 -0700 Subject: [PATCH 16/28] Wire up request/response --- livekit/src/rtc_engine/mod.rs | 92 +++++++++++++-------------- livekit/src/rtc_engine/rtc_session.rs | 39 +++++++++--- 2 files changed, 76 insertions(+), 55 deletions(-) diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index 244e12dd5..df29e4256 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -295,28 +295,27 @@ impl RtcEngine { ) -> EngineResult<()> { let blob = proto::DataBlob { key: Some(key), contents: contents.into() }; - let request_id = self.session().signal_client().next_request_id(); - // TODO: `StoreDataBlobRequest` is missing a `request_id` field. Until it is added - // (and echoed back by the server on the `RequestResponse`), the response awaited - // below cannot be correlated to this request and the call will time out. - // Set `request_id` on the request once the proto field exists. - let request = proto::StoreDataBlobRequest { blob: Some(blob) }; + let session = self.session(); + let request_id = session.signal_client().next_request_id(); + let request = proto::StoreDataBlobRequest { blob: Some(blob), request_id }; self.send_request(proto::signal_request::Message::StoreDataBlobRequest(request)).await; - let response = timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, self.get_response(request_id)) - .await - .map_err(|_| { - EngineError::Signal(SignalError::Timeout( - "store data blob request timed out".into(), - )) - })?; - - match response.reason() { - proto::request_response::Reason::Ok => Ok(()), - reason => Err(EngineError::Internal( - format!("store data blob request failed ({reason:?}): {}", response.message).into(), - )), - } + livekit_runtime::spawn(async move { + let Ok(response) = + timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, session.get_response(request_id)).await + else { + return; // No error arrived within the window: treat the store as successful. + }; + if response.reason() != proto::request_response::Reason::Ok { + log::error!( + "store data blob failed ({:?}): {}", + response.reason(), + response.message + ); + } + }); + + Ok(()) } pub async fn get_data_blob( @@ -324,30 +323,36 @@ impl RtcEngine { key: proto::DataBlobKey, participant: ParticipantIdentity, ) -> EngineResult { - let request_id = self.session().signal_client().next_request_id(); - // TODO: `GetDataBlobRequest` and `GetDataBlobResponse` are both missing a - // `request_id` field. Until they are added (and echoed back by the server on the - // response), the response awaited below cannot be correlated to this request and - // the call will time out. Set `request_id` on the request once the proto field - // exists, and resolve the pending request in the `GetDataBlobResponse` handler in - // `rtc_session.rs`. - let request = - proto::GetDataBlobRequest { key: Some(key), participant_identity: participant.0 }; + let session = self.session(); + let request_id = session.signal_client().next_request_id(); + let request = proto::GetDataBlobRequest { + key: Some(key), + participant_identity: participant.0, + request_id, + }; self.send_request(proto::signal_request::Message::GetDataBlobRequest(request)).await; - let response = - timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, self.get_data_blob_response(request_id)) - .await - .map_err(|_| { - EngineError::Signal(SignalError::Timeout( - "get data blob request timed out".into(), - )) + let response = timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, async { + tokio::select! { + response = session.get_data_blob_response(request_id) => Ok(response), + error = session.get_response(request_id) => Err(error), + } + }) + .await + .map_err(|_| EngineError::Signal(SignalError::Timeout("get data blob timed out".into())))?; + + match response { + Ok(response) => { + let blob = response.blob.ok_or_else(|| { + EngineError::Internal("get data blob response is malformed".into()) })?; - - let blob = response.blob.ok_or_else(|| { - EngineError::Internal("get data blob response is missing the blob".into()) - })?; - Ok(blob.contents.into()) + Ok(blob.contents.into()) + } + Err(error) => Err(EngineError::Internal( + format!("get data blob request failed ({:?}): {}", error.reason(), error.message) + .into(), + )), + } } pub async fn simulate_scenario(&self, scenario: SimulateScenario) -> EngineResult<()> { @@ -444,11 +449,6 @@ impl RtcEngine { session.get_response(request_id).await } - pub async fn get_data_blob_response(&self, request_id: u32) -> proto::GetDataBlobResponse { - let session = self.inner.running_handle.read().session.clone(); - session.get_data_blob_response(request_id).await - } - pub async fn get_stats(&self) -> EngineResult { let session = self.inner.running_handle.read().session.clone(); session.get_stats().await diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 2abcc5d4d..a158e33ae 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -1338,15 +1338,12 @@ impl SessionInner { self.handle_media_sections_requirement(req)?; } } - proto::signal_response::Message::GetDataBlobResponse(_response) => { - // TODO: `GetDataBlobResponse` is missing a `request_id` field, so the - // response cannot be correlated with the originating `GetDataBlobRequest`. - // Once the field exists, resolve the matching pending request, e.g.: - // if let Some(tx) = - // self.pending_data_blob_requests.lock().remove(&_response.request_id) - // { - // let _ = tx.send(_response); - // } + proto::signal_response::Message::GetDataBlobResponse(response) => { + if let Some(tx) = + self.pending_data_blob_requests.lock().remove(&response.request_id) + { + let _ = tx.send(response); + } } _ => {} } @@ -2262,16 +2259,40 @@ impl SessionInner { async fn get_response(&self, request_id: u32) -> proto::RequestResponse { let (tx, rx) = oneshot::channel(); self.pending_requests.lock().insert(request_id, tx); + let _guard = PendingResponseGuard::new(&self.pending_requests, request_id); rx.await.unwrap() } async fn get_data_blob_response(&self, request_id: u32) -> proto::GetDataBlobResponse { let (tx, rx) = oneshot::channel(); self.pending_data_blob_requests.lock().insert(request_id, tx); + let _guard = PendingResponseGuard::new(&self.pending_data_blob_requests, request_id); rx.await.expect("data blob response sender dropped") } } +/// Removes a pending response registration when dropped. +/// +/// Installed alongside a registration so that abandoning the wait (e.g. a timeout or a +/// losing [`tokio::select!`] branch) cannot leave a stale entry behind. Dropping after the +/// response has already been delivered is a no-op since the entry is removed on delivery. +struct PendingResponseGuard<'a, T> { + map: &'a Mutex>>, + request_id: u32, +} + +impl<'a, T> PendingResponseGuard<'a, T> { + fn new(map: &'a Mutex>>, request_id: u32) -> Self { + Self { map, request_id } + } +} + +impl Drop for PendingResponseGuard<'_, T> { + fn drop(&mut self) { + self.map.lock().remove(&self.request_id); + } +} + /// Emit incoming data track packets as session events. pub fn handle_remote_dt_packets(dc: &DataChannel, emitter: WeakUnboundedSender) { let on_message: libwebrtc::data_channel::OnMessage = Box::new(move |buffer: DataBuffer| { From fc19418c20af1dc59ce2cbe64c03d35ce8ae7c72 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:56:15 -0700 Subject: [PATCH 17/28] Update protocol --- livekit-protocol/protocol | 2 +- livekit-protocol/src/livekit.rs | 51 ++++---- livekit-protocol/src/livekit.serde.rs | 167 ++++++++++++++------------ 3 files changed, 116 insertions(+), 104 deletions(-) diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index 41c46d1fa..bb0475b96 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit 41c46d1fa667e45c606741e97b69ba6fddf2f2da +Subproject commit bb0475b96d1ee1b18168be3643233b6cc21ed4b8 diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index 81b10bb1d..00e329426 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -2358,10 +2358,9 @@ pub struct WebSource { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct MediaSource { + /// TODO: DataConfig data = 4; #[prost(message, optional, tag="3")] pub audio: ::core::option::Option, - #[prost(message, optional, tag="4")] - pub data: ::core::option::Option, #[prost(oneof="media_source::Video", tags="1, 2")] pub video: ::core::option::Option, } @@ -2376,6 +2375,8 @@ pub mod media_source { ParticipantVideo(super::ParticipantVideo), } } +// --- Video Configuration --- + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ParticipantVideo { @@ -2391,9 +2392,10 @@ pub struct ParticipantVideo { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AudioConfig { - /// If empty, all audio captured in both channels. - /// If non-empty, only matching audio is captured and routed. Unmatched is excluded. - #[prost(message, repeated, tag="1")] + /// If true, all unmatched audio is recorded to both channels + #[prost(bool, tag="1")] + pub capture_all: bool, + #[prost(message, repeated, tag="2")] pub routes: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] @@ -2422,15 +2424,15 @@ pub mod audio_route { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DataConfig { - /// If empty, all data tracks captured. - /// If non-empty, only matching data tracks are captured. - #[prost(message, repeated, tag="1")] + #[prost(bool, tag="1")] + pub capture_all: bool, + #[prost(message, repeated, tag="2")] pub selectors: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DataSelector { - #[prost(oneof="data_selector::Match", tags="1, 2, 3")] + #[prost(oneof="data_selector::Match", tags="1, 2")] pub r#match: ::core::option::Option, } /// Nested message and enum types in `DataSelector`. @@ -2442,8 +2444,6 @@ pub mod data_selector { TrackId(::prost::alloc::string::String), #[prost(string, tag="2")] ParticipantIdentity(::prost::alloc::string::String), - #[prost(string, tag="3")] - Topic(::prost::alloc::string::String), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -2512,7 +2512,7 @@ pub mod output { Stream(super::StreamOutput), #[prost(message, tag="3")] Segments(super::SegmentedFileOutput), - /// 5 reserved for mcap; + /// TODO: DataOutput data = 5; #[prost(message, tag="4")] Images(super::ImageOutput), } @@ -2564,11 +2564,13 @@ pub struct SegmentedFileOutput { /// disable upload of manifest file (default false) #[prost(bool, tag="8")] pub disable_manifest: bool, + /// TODO: deprecate #[prost(oneof="segmented_file_output::Output", tags="5, 6, 7, 9")] pub output: ::core::option::Option, } /// Nested message and enum types in `SegmentedFileOutput`. pub mod segmented_file_output { + /// TODO: deprecate #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Output { @@ -2607,11 +2609,13 @@ pub struct ImageOutput { /// disable upload of manifest file (default false) #[prost(bool, tag="7")] pub disable_manifest: bool, + /// TODO: deprecate #[prost(oneof="image_output::Output", tags="8, 9, 10, 11")] pub output: ::core::option::Option, } /// Nested message and enum types in `ImageOutput`. pub mod image_output { + /// TODO: deprecate #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Output { @@ -2831,6 +2835,7 @@ pub mod egress_info { Egress(super::StartEgressRequest), #[prost(message, tag="30")] Replay(super::ExportReplayRequest), + /// TODO: deprecate #[prost(message, tag="4")] RoomComposite(super::RoomCompositeEgressRequest), #[prost(message, tag="14")] @@ -3053,7 +3058,7 @@ pub mod export_replay_request { Advanced(super::EncodingOptions), } } -// --- V1 --- +// TODO: deprecate --- V1 --- #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -4105,30 +4110,30 @@ pub mod update_data_subscription { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct StoreDataBlobRequest { - #[prost(message, optional, tag="1")] - pub blob: ::core::option::Option, - #[prost(uint32, tag="2")] + #[prost(uint32, tag="1")] pub request_id: u32, + #[prost(message, optional, tag="2")] + pub blob: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetDataBlobRequest { + #[prost(uint32, tag="1")] + pub request_id: u32, /// Identity of the participant who owns the blob. - #[prost(string, tag="1")] + #[prost(string, tag="2")] pub participant_identity: ::prost::alloc::string::String, /// Unique key of the data blob to retrieve. - #[prost(message, optional, tag="2")] + #[prost(message, optional, tag="3")] pub key: ::core::option::Option, - #[prost(uint32, tag="3")] - pub request_id: u32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetDataBlobResponse { - #[prost(message, optional, tag="1")] - pub blob: ::core::option::Option, - #[prost(uint32, tag="2")] + #[prost(uint32, tag="1")] pub request_id: u32, + #[prost(message, optional, tag="2")] + pub blob: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index e2e040b48..2974076cf 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -4906,10 +4906,16 @@ impl serde::Serialize for AudioConfig { { use serde::ser::SerializeStruct; let mut len = 0; + if self.capture_all { + len += 1; + } if !self.routes.is_empty() { len += 1; } let mut struct_ser = serializer.serialize_struct("livekit.AudioConfig", len)?; + if self.capture_all { + struct_ser.serialize_field("captureAll", &self.capture_all)?; + } if !self.routes.is_empty() { struct_ser.serialize_field("routes", &self.routes)?; } @@ -4923,11 +4929,14 @@ impl<'de> serde::Deserialize<'de> for AudioConfig { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ + "capture_all", + "captureAll", "routes", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { + CaptureAll, Routes, __SkipField__, } @@ -4951,6 +4960,7 @@ impl<'de> serde::Deserialize<'de> for AudioConfig { E: serde::de::Error, { match value { + "captureAll" | "capture_all" => Ok(GeneratedField::CaptureAll), "routes" => Ok(GeneratedField::Routes), _ => Ok(GeneratedField::__SkipField__), } @@ -4971,9 +4981,16 @@ impl<'de> serde::Deserialize<'de> for AudioConfig { where V: serde::de::MapAccess<'de>, { + let mut capture_all__ = None; let mut routes__ = None; while let Some(k) = map_.next_key()? { match k { + GeneratedField::CaptureAll => { + if capture_all__.is_some() { + return Err(serde::de::Error::duplicate_field("captureAll")); + } + capture_all__ = Some(map_.next_value()?); + } GeneratedField::Routes => { if routes__.is_some() { return Err(serde::de::Error::duplicate_field("routes")); @@ -4986,6 +5003,7 @@ impl<'de> serde::Deserialize<'de> for AudioConfig { } } Ok(AudioConfig { + capture_all: capture_all__.unwrap_or_default(), routes: routes__.unwrap_or_default(), }) } @@ -10954,10 +10972,16 @@ impl serde::Serialize for DataConfig { { use serde::ser::SerializeStruct; let mut len = 0; + if self.capture_all { + len += 1; + } if !self.selectors.is_empty() { len += 1; } let mut struct_ser = serializer.serialize_struct("livekit.DataConfig", len)?; + if self.capture_all { + struct_ser.serialize_field("captureAll", &self.capture_all)?; + } if !self.selectors.is_empty() { struct_ser.serialize_field("selectors", &self.selectors)?; } @@ -10971,11 +10995,14 @@ impl<'de> serde::Deserialize<'de> for DataConfig { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ + "capture_all", + "captureAll", "selectors", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { + CaptureAll, Selectors, __SkipField__, } @@ -10999,6 +11026,7 @@ impl<'de> serde::Deserialize<'de> for DataConfig { E: serde::de::Error, { match value { + "captureAll" | "capture_all" => Ok(GeneratedField::CaptureAll), "selectors" => Ok(GeneratedField::Selectors), _ => Ok(GeneratedField::__SkipField__), } @@ -11019,9 +11047,16 @@ impl<'de> serde::Deserialize<'de> for DataConfig { where V: serde::de::MapAccess<'de>, { + let mut capture_all__ = None; let mut selectors__ = None; while let Some(k) = map_.next_key()? { match k { + GeneratedField::CaptureAll => { + if capture_all__.is_some() { + return Err(serde::de::Error::duplicate_field("captureAll")); + } + capture_all__ = Some(map_.next_value()?); + } GeneratedField::Selectors => { if selectors__.is_some() { return Err(serde::de::Error::duplicate_field("selectors")); @@ -11034,6 +11069,7 @@ impl<'de> serde::Deserialize<'de> for DataConfig { } } Ok(DataConfig { + capture_all: capture_all__.unwrap_or_default(), selectors: selectors__.unwrap_or_default(), }) } @@ -11489,9 +11525,6 @@ impl serde::Serialize for DataSelector { data_selector::Match::ParticipantIdentity(v) => { struct_ser.serialize_field("participantIdentity", v)?; } - data_selector::Match::Topic(v) => { - struct_ser.serialize_field("topic", v)?; - } } } struct_ser.end() @@ -11508,14 +11541,12 @@ impl<'de> serde::Deserialize<'de> for DataSelector { "trackId", "participant_identity", "participantIdentity", - "topic", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { TrackId, ParticipantIdentity, - Topic, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -11540,7 +11571,6 @@ impl<'de> serde::Deserialize<'de> for DataSelector { match value { "trackId" | "track_id" => Ok(GeneratedField::TrackId), "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), - "topic" => Ok(GeneratedField::Topic), _ => Ok(GeneratedField::__SkipField__), } } @@ -11575,12 +11605,6 @@ impl<'de> serde::Deserialize<'de> for DataSelector { } r#match__ = map_.next_value::<::std::option::Option<_>>()?.map(data_selector::Match::ParticipantIdentity); } - GeneratedField::Topic => { - if r#match__.is_some() { - return Err(serde::de::Error::duplicate_field("topic")); - } - r#match__ = map_.next_value::<::std::option::Option<_>>()?.map(data_selector::Match::Topic); - } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -18904,25 +18928,25 @@ impl serde::Serialize for GetDataBlobRequest { { use serde::ser::SerializeStruct; let mut len = 0; + if self.request_id != 0 { + len += 1; + } if !self.participant_identity.is_empty() { len += 1; } if self.key.is_some() { len += 1; } + let mut struct_ser = serializer.serialize_struct("livekit.GetDataBlobRequest", len)?; if self.request_id != 0 { - len += 1; + struct_ser.serialize_field("requestId", &self.request_id)?; } - let mut struct_ser = serializer.serialize_struct("livekit.GetDataBlobRequest", len)?; if !self.participant_identity.is_empty() { struct_ser.serialize_field("participantIdentity", &self.participant_identity)?; } if let Some(v) = self.key.as_ref() { struct_ser.serialize_field("key", v)?; } - if self.request_id != 0 { - struct_ser.serialize_field("requestId", &self.request_id)?; - } struct_ser.end() } } @@ -18933,18 +18957,18 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobRequest { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ + "request_id", + "requestId", "participant_identity", "participantIdentity", "key", - "request_id", - "requestId", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { + RequestId, ParticipantIdentity, Key, - RequestId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -18967,9 +18991,9 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobRequest { E: serde::de::Error, { match value { + "requestId" | "request_id" => Ok(GeneratedField::RequestId), "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), "key" => Ok(GeneratedField::Key), - "requestId" | "request_id" => Ok(GeneratedField::RequestId), _ => Ok(GeneratedField::__SkipField__), } } @@ -18989,11 +19013,19 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobRequest { where V: serde::de::MapAccess<'de>, { + let mut request_id__ = None; let mut participant_identity__ = None; let mut key__ = None; - let mut request_id__ = None; while let Some(k) = map_.next_key()? { match k { + GeneratedField::RequestId => { + if request_id__.is_some() { + return Err(serde::de::Error::duplicate_field("requestId")); + } + request_id__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } GeneratedField::ParticipantIdentity => { if participant_identity__.is_some() { return Err(serde::de::Error::duplicate_field("participantIdentity")); @@ -19006,23 +19038,15 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobRequest { } key__ = map_.next_value()?; } - GeneratedField::RequestId => { - if request_id__.is_some() { - return Err(serde::de::Error::duplicate_field("requestId")); - } - request_id__ = - Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) - ; - } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } Ok(GetDataBlobRequest { + request_id: request_id__.unwrap_or_default(), participant_identity: participant_identity__.unwrap_or_default(), key: key__, - request_id: request_id__.unwrap_or_default(), }) } } @@ -19037,19 +19061,19 @@ impl serde::Serialize for GetDataBlobResponse { { use serde::ser::SerializeStruct; let mut len = 0; - if self.blob.is_some() { + if self.request_id != 0 { len += 1; } - if self.request_id != 0 { + if self.blob.is_some() { len += 1; } let mut struct_ser = serializer.serialize_struct("livekit.GetDataBlobResponse", len)?; - if let Some(v) = self.blob.as_ref() { - struct_ser.serialize_field("blob", v)?; - } if self.request_id != 0 { struct_ser.serialize_field("requestId", &self.request_id)?; } + if let Some(v) = self.blob.as_ref() { + struct_ser.serialize_field("blob", v)?; + } struct_ser.end() } } @@ -19060,15 +19084,15 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobResponse { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "blob", "request_id", "requestId", + "blob", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Blob, RequestId, + Blob, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -19091,8 +19115,8 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobResponse { E: serde::de::Error, { match value { - "blob" => Ok(GeneratedField::Blob), "requestId" | "request_id" => Ok(GeneratedField::RequestId), + "blob" => Ok(GeneratedField::Blob), _ => Ok(GeneratedField::__SkipField__), } } @@ -19112,16 +19136,10 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobResponse { where V: serde::de::MapAccess<'de>, { - let mut blob__ = None; let mut request_id__ = None; + let mut blob__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Blob => { - if blob__.is_some() { - return Err(serde::de::Error::duplicate_field("blob")); - } - blob__ = map_.next_value()?; - } GeneratedField::RequestId => { if request_id__.is_some() { return Err(serde::de::Error::duplicate_field("requestId")); @@ -19130,14 +19148,20 @@ impl<'de> serde::Deserialize<'de> for GetDataBlobResponse { Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) ; } + GeneratedField::Blob => { + if blob__.is_some() { + return Err(serde::de::Error::duplicate_field("blob")); + } + blob__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } Ok(GetDataBlobResponse { - blob: blob__, request_id: request_id__.unwrap_or_default(), + blob: blob__, }) } } @@ -26127,9 +26151,6 @@ impl serde::Serialize for MediaSource { if self.audio.is_some() { len += 1; } - if self.data.is_some() { - len += 1; - } if self.video.is_some() { len += 1; } @@ -26137,9 +26158,6 @@ impl serde::Serialize for MediaSource { if let Some(v) = self.audio.as_ref() { struct_ser.serialize_field("audio", v)?; } - if let Some(v) = self.data.as_ref() { - struct_ser.serialize_field("data", v)?; - } if let Some(v) = self.video.as_ref() { match v { media_source::Video::VideoTrackId(v) => { @@ -26161,7 +26179,6 @@ impl<'de> serde::Deserialize<'de> for MediaSource { { const FIELDS: &[&str] = &[ "audio", - "data", "video_track_id", "videoTrackId", "participant_video", @@ -26171,7 +26188,6 @@ impl<'de> serde::Deserialize<'de> for MediaSource { #[allow(clippy::enum_variant_names)] enum GeneratedField { Audio, - Data, VideoTrackId, ParticipantVideo, __SkipField__, @@ -26197,7 +26213,6 @@ impl<'de> serde::Deserialize<'de> for MediaSource { { match value { "audio" => Ok(GeneratedField::Audio), - "data" => Ok(GeneratedField::Data), "videoTrackId" | "video_track_id" => Ok(GeneratedField::VideoTrackId), "participantVideo" | "participant_video" => Ok(GeneratedField::ParticipantVideo), _ => Ok(GeneratedField::__SkipField__), @@ -26220,7 +26235,6 @@ impl<'de> serde::Deserialize<'de> for MediaSource { V: serde::de::MapAccess<'de>, { let mut audio__ = None; - let mut data__ = None; let mut video__ = None; while let Some(k) = map_.next_key()? { match k { @@ -26230,12 +26244,6 @@ impl<'de> serde::Deserialize<'de> for MediaSource { } audio__ = map_.next_value()?; } - GeneratedField::Data => { - if data__.is_some() { - return Err(serde::de::Error::duplicate_field("data")); - } - data__ = map_.next_value()?; - } GeneratedField::VideoTrackId => { if video__.is_some() { return Err(serde::de::Error::duplicate_field("videoTrackId")); @@ -26256,7 +26264,6 @@ impl<'de> serde::Deserialize<'de> for MediaSource { } Ok(MediaSource { audio: audio__, - data: data__, video: video__, }) } @@ -46009,19 +46016,19 @@ impl serde::Serialize for StoreDataBlobRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if self.blob.is_some() { + if self.request_id != 0 { len += 1; } - if self.request_id != 0 { + if self.blob.is_some() { len += 1; } let mut struct_ser = serializer.serialize_struct("livekit.StoreDataBlobRequest", len)?; - if let Some(v) = self.blob.as_ref() { - struct_ser.serialize_field("blob", v)?; - } if self.request_id != 0 { struct_ser.serialize_field("requestId", &self.request_id)?; } + if let Some(v) = self.blob.as_ref() { + struct_ser.serialize_field("blob", v)?; + } struct_ser.end() } } @@ -46032,15 +46039,15 @@ impl<'de> serde::Deserialize<'de> for StoreDataBlobRequest { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "blob", "request_id", "requestId", + "blob", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Blob, RequestId, + Blob, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -46063,8 +46070,8 @@ impl<'de> serde::Deserialize<'de> for StoreDataBlobRequest { E: serde::de::Error, { match value { - "blob" => Ok(GeneratedField::Blob), "requestId" | "request_id" => Ok(GeneratedField::RequestId), + "blob" => Ok(GeneratedField::Blob), _ => Ok(GeneratedField::__SkipField__), } } @@ -46084,16 +46091,10 @@ impl<'de> serde::Deserialize<'de> for StoreDataBlobRequest { where V: serde::de::MapAccess<'de>, { - let mut blob__ = None; let mut request_id__ = None; + let mut blob__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Blob => { - if blob__.is_some() { - return Err(serde::de::Error::duplicate_field("blob")); - } - blob__ = map_.next_value()?; - } GeneratedField::RequestId => { if request_id__.is_some() { return Err(serde::de::Error::duplicate_field("requestId")); @@ -46102,14 +46103,20 @@ impl<'de> serde::Deserialize<'de> for StoreDataBlobRequest { Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) ; } + GeneratedField::Blob => { + if blob__.is_some() { + return Err(serde::de::Error::duplicate_field("blob")); + } + blob__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } Ok(StoreDataBlobRequest { - blob: blob__, request_id: request_id__.unwrap_or_default(), + blob: blob__, }) } } From 5ab44b2af835f03b6442fdb594a56b601143ba8b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:27:05 -0700 Subject: [PATCH 18/28] Move data blob methods to local participant --- .../src/room/participant/local_participant.rs | 91 ++++++++++++++++++- livekit/src/rtc_engine/mod.rs | 73 +-------------- 2 files changed, 88 insertions(+), 76 deletions(-) diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 287d11089..0fb9e981d 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -37,9 +37,10 @@ use crate::{ prelude::*, room::rpc::{RpcError, RpcErrorCode, RpcInvocationData}, rtc_engine::lk_runtime::LkRuntime, - rtc_engine::{EngineError, RtcEngine}, + rtc_engine::{EngineError, EngineResult, RtcEngine}, ChatMessage, DataPacket, RoomSession, SipDTMF, Transcription, }; +use bytes::Bytes; use chrono::Utc; use libwebrtc::{ native::{create_random_uuid, packet_trailer}, @@ -937,7 +938,7 @@ impl LocalParticipant { /// the caller is responsible for ensuring it is well-formed. /// pub async fn define_schema(&self, id: DataTrackSchemaId, definition: String) -> RoomResult<()> { - self.inner.rtc_engine.store_data_blob(id.into(), definition.into()).await?; + self.store_data_blob(id.into(), definition.into()).await?; Ok(()) } @@ -958,8 +959,6 @@ impl LocalParticipant { participant: ParticipantIdentity, ) -> RoomResult { let contents = self - .inner - .rtc_engine .get_data_blob(id.into(), participant) .await .inspect_err(|err| log::error!("Failed to get schema: {err}"))?; @@ -969,6 +968,90 @@ impl LocalParticipant { })?; Ok(definition) } + + // TODO: unify request/response logic, timeout behavior across SDK. + const DATA_BLOB_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); + + /// Stores an arbitrary blob of data on the server, keyed by `key`. + /// + /// This is a lower-level primitive; most callers should prefer the schema + /// helpers (e.g. [`define_schema`](Self::define_schema)). + pub async fn store_data_blob( + &self, + key: proto::DataBlobKey, + contents: Bytes, + ) -> EngineResult<()> { + let blob = proto::DataBlob { key: Some(key), contents: contents.into() }; + + let session = self.inner.rtc_engine.session(); + let request_id = session.signal_client().next_request_id(); + let request = proto::StoreDataBlobRequest { blob: Some(blob), request_id }; + self.inner + .rtc_engine + .send_request(proto::signal_request::Message::StoreDataBlobRequest(request)) + .await; + + livekit_runtime::spawn(async move { + let Ok(response) = + timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, session.get_response(request_id)).await + else { + return; // No error arrived within the window: treat the store as successful. + }; + if response.reason() != Reason::Ok { + log::error!( + "store data blob failed ({:?}): {}", + response.reason(), + response.message + ); + } + }); + + Ok(()) + } + + /// Retrieves a blob of data previously stored by `participant` under `key`. + /// + /// This is a lower-level primitive; most callers should prefer the schema + /// helpers (e.g. [`get_schema`](Self::get_schema)). + pub async fn get_data_blob( + &self, + key: proto::DataBlobKey, + participant: ParticipantIdentity, + ) -> EngineResult { + let session = self.inner.rtc_engine.session(); + let request_id = session.signal_client().next_request_id(); + let request = proto::GetDataBlobRequest { + key: Some(key), + participant_identity: participant.0, + request_id, + }; + self.inner + .rtc_engine + .send_request(proto::signal_request::Message::GetDataBlobRequest(request)) + .await; + + let response = timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, async { + tokio::select! { + response = session.get_data_blob_response(request_id) => Ok(response), + error = session.get_response(request_id) => Err(error), + } + }) + .await + .map_err(|_| EngineError::Signal(SignalError::Timeout("get data blob timed out".into())))?; + + match response { + Ok(response) => { + let blob = response.blob.ok_or_else(|| { + EngineError::Internal("get data blob response is malformed".into()) + })?; + Ok(blob.contents.into()) + } + Err(error) => Err(EngineError::Internal( + format!("get data blob request failed ({:?}): {}", error.reason(), error.message) + .into(), + )), + } + } } #[cfg(test)] diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index df29e4256..f560ef554 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use bytes::Bytes; use libwebrtc::prelude::*; use livekit_api::signal_client::{SignalError, SignalOptions}; use livekit_datatrack::backend as dt; use livekit_protocol as proto; -use livekit_runtime::{interval, timeout, Interval, JoinHandle, MissedTickBehavior}; +use livekit_runtime::{interval, Interval, JoinHandle, MissedTickBehavior}; use parking_lot::{RwLock, RwLockReadGuard}; use std::{borrow::Cow, fmt::Debug, sync::Arc, time::Duration}; use thiserror::Error; @@ -285,76 +284,6 @@ impl RtcEngine { session.publish_data(data, kind, is_raw_packet).await } - // TODO: unify request/response logic, timeout behavior across SDK. - const DATA_BLOB_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); - - pub async fn store_data_blob( - &self, - key: proto::DataBlobKey, - contents: Bytes, - ) -> EngineResult<()> { - let blob = proto::DataBlob { key: Some(key), contents: contents.into() }; - - let session = self.session(); - let request_id = session.signal_client().next_request_id(); - let request = proto::StoreDataBlobRequest { blob: Some(blob), request_id }; - self.send_request(proto::signal_request::Message::StoreDataBlobRequest(request)).await; - - livekit_runtime::spawn(async move { - let Ok(response) = - timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, session.get_response(request_id)).await - else { - return; // No error arrived within the window: treat the store as successful. - }; - if response.reason() != proto::request_response::Reason::Ok { - log::error!( - "store data blob failed ({:?}): {}", - response.reason(), - response.message - ); - } - }); - - Ok(()) - } - - pub async fn get_data_blob( - &self, - key: proto::DataBlobKey, - participant: ParticipantIdentity, - ) -> EngineResult { - let session = self.session(); - let request_id = session.signal_client().next_request_id(); - let request = proto::GetDataBlobRequest { - key: Some(key), - participant_identity: participant.0, - request_id, - }; - self.send_request(proto::signal_request::Message::GetDataBlobRequest(request)).await; - - let response = timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, async { - tokio::select! { - response = session.get_data_blob_response(request_id) => Ok(response), - error = session.get_response(request_id) => Err(error), - } - }) - .await - .map_err(|_| EngineError::Signal(SignalError::Timeout("get data blob timed out".into())))?; - - match response { - Ok(response) => { - let blob = response.blob.ok_or_else(|| { - EngineError::Internal("get data blob response is malformed".into()) - })?; - Ok(blob.contents.into()) - } - Err(error) => Err(EngineError::Internal( - format!("get data blob request failed ({:?}): {}", error.reason(), error.message) - .into(), - )), - } - } - pub async fn simulate_scenario(&self, scenario: SimulateScenario) -> EngineResult<()> { let (session, _r_lock) = { let (handle, _r_lock) = self.inner.wait_reconnection().await?; From ea631249e1e437629a782e690e3202e523b78f93 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:46:55 -0700 Subject: [PATCH 19/28] Update protocol --- livekit-protocol/protocol | 2 +- livekit-protocol/src/livekit.rs | 19 +++- livekit-protocol/src/livekit.serde.rs | 129 ++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 3 deletions(-) diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index bb0475b96..2de4579fd 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit bb0475b96d1ee1b18168be3643233b6cc21ed4b8 +Subproject commit 2de4579fdfc57e56869b29eb7635bb6a8ba74e42 diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index 00e329426..95ee01ab4 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -643,6 +643,7 @@ pub struct DataTrackInfo { /// /// Schemas with the same name but different encodings are distinct. /// +#[derive(Eq)] #[derive(Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DataTrackSchemaId { @@ -669,6 +670,7 @@ pub struct DataTrackSubscriptionOptions { pub target_fps: ::core::option::Option, } /// Key used to uniquely identify a data blob for storage and retrieval. +#[derive(Eq)] #[derive(Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DataBlobKey { @@ -677,6 +679,7 @@ pub struct DataBlobKey { } /// Nested message and enum types in `DataBlobKey`. pub mod data_blob_key { + #[derive(Eq)] #[derive(Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Key { @@ -3744,7 +3747,7 @@ pub mod signal_request { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SignalResponse { - #[prost(oneof="signal_response::Message", tags="1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30")] + #[prost(oneof="signal_response::Message", tags="1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalResponse`. @@ -3839,8 +3842,11 @@ pub mod signal_response { /// Sent to data track subscribers to provide mapping from track SIDs to handles. #[prost(message, tag="29")] DataTrackSubscriberHandles(super::DataTrackSubscriberHandles), - /// Sent in response to `GetDataBlobRequest`. + /// Sent in response to `StoreDataBlobRequest`. #[prost(message, tag="30")] + StoreDataBlobResponse(super::StoreDataBlobResponse), + /// Sent in response to `GetDataBlobRequest`. + #[prost(message, tag="31")] GetDataBlobResponse(super::GetDataBlobResponse), } } @@ -4117,6 +4123,15 @@ pub struct StoreDataBlobRequest { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct StoreDataBlobResponse { + #[prost(uint32, tag="1")] + pub request_id: u32, + /// Unique key the data blob was stored under. + #[prost(message, optional, tag="2")] + pub key: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct GetDataBlobRequest { #[prost(uint32, tag="1")] pub request_id: u32, diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index 2974076cf..dd9b815d7 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -44075,6 +44075,9 @@ impl serde::Serialize for SignalResponse { signal_response::Message::DataTrackSubscriberHandles(v) => { struct_ser.serialize_field("dataTrackSubscriberHandles", v)?; } + signal_response::Message::StoreDataBlobResponse(v) => { + struct_ser.serialize_field("storeDataBlobResponse", v)?; + } signal_response::Message::GetDataBlobResponse(v) => { struct_ser.serialize_field("getDataBlobResponse", v)?; } @@ -44137,6 +44140,8 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "unpublishDataTrackResponse", "data_track_subscriber_handles", "dataTrackSubscriberHandles", + "store_data_blob_response", + "storeDataBlobResponse", "get_data_blob_response", "getDataBlobResponse", ]; @@ -44171,6 +44176,7 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { PublishDataTrackResponse, UnpublishDataTrackResponse, DataTrackSubscriberHandles, + StoreDataBlobResponse, GetDataBlobResponse, __SkipField__, } @@ -44222,6 +44228,7 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "publishDataTrackResponse" | "publish_data_track_response" => Ok(GeneratedField::PublishDataTrackResponse), "unpublishDataTrackResponse" | "unpublish_data_track_response" => Ok(GeneratedField::UnpublishDataTrackResponse), "dataTrackSubscriberHandles" | "data_track_subscriber_handles" => Ok(GeneratedField::DataTrackSubscriberHandles), + "storeDataBlobResponse" | "store_data_blob_response" => Ok(GeneratedField::StoreDataBlobResponse), "getDataBlobResponse" | "get_data_blob_response" => Ok(GeneratedField::GetDataBlobResponse), _ => Ok(GeneratedField::__SkipField__), } @@ -44437,6 +44444,13 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { return Err(serde::de::Error::duplicate_field("dataTrackSubscriberHandles")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::DataTrackSubscriberHandles) +; + } + GeneratedField::StoreDataBlobResponse => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("storeDataBlobResponse")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::StoreDataBlobResponse) ; } GeneratedField::GetDataBlobResponse => { @@ -46123,6 +46137,121 @@ impl<'de> serde::Deserialize<'de> for StoreDataBlobRequest { deserializer.deserialize_struct("livekit.StoreDataBlobRequest", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for StoreDataBlobResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.request_id != 0 { + len += 1; + } + if self.key.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.StoreDataBlobResponse", len)?; + if self.request_id != 0 { + struct_ser.serialize_field("requestId", &self.request_id)?; + } + if let Some(v) = self.key.as_ref() { + struct_ser.serialize_field("key", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for StoreDataBlobResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "request_id", + "requestId", + "key", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + RequestId, + Key, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "requestId" | "request_id" => Ok(GeneratedField::RequestId), + "key" => Ok(GeneratedField::Key), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = StoreDataBlobResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.StoreDataBlobResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut request_id__ = None; + let mut key__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::RequestId => { + if request_id__.is_some() { + return Err(serde::de::Error::duplicate_field("requestId")); + } + request_id__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Key => { + if key__.is_some() { + return Err(serde::de::Error::duplicate_field("key")); + } + key__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(StoreDataBlobResponse { + request_id: request_id__.unwrap_or_default(), + key: key__, + }) + } + } + deserializer.deserialize_struct("livekit.StoreDataBlobResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for StreamInfo { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result From ab8d654b92c719c725015802eb9b0e9e4587cb05 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:37:43 -0700 Subject: [PATCH 20/28] Derive eq and hash on data blob proto types --- livekit-protocol/generate_proto.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/livekit-protocol/generate_proto.sh b/livekit-protocol/generate_proto.sh index e3df93b23..9b8a65ade 100755 --- a/livekit-protocol/generate_proto.sh +++ b/livekit-protocol/generate_proto.sh @@ -24,6 +24,9 @@ protoc \ --prost_out=$OUT_RUST \ --prost_opt=compile_well_known_types \ --prost_opt=extern_path=.google.protobuf=::pbjson_types \ + '--prost_opt=type_attribute=livekit.DataBlobKey=#[derive(Eq)] #[derive(Hash)]' \ + '--prost_opt=type_attribute=livekit.DataBlobKey.key=#[derive(Eq)] #[derive(Hash)]' \ + '--prost_opt=type_attribute=livekit.DataTrackSchemaId=#[derive(Eq)] #[derive(Hash)]' \ --prost-serde_out=$OUT_RUST \ --prost-serde_opt=ignore_unknown_fields \ $PROTOCOL/livekit_egress.proto \ From c31b6fd645dc87bb784b94c90e35b3cf375a353e Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:38:00 -0700 Subject: [PATCH 21/28] Wire up --- .../src/room/participant/local_participant.rs | 52 +++++----- livekit/src/rtc_engine/rtc_session.rs | 60 ++++++++---- livekit/tests/data_blob_test.rs | 94 +++++++++++++++++++ livekit/tests/data_track_test.rs | 2 + 4 files changed, 170 insertions(+), 38 deletions(-) create mode 100644 livekit/tests/data_blob_test.rs diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 0fb9e981d..2599b36ea 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -973,9 +973,6 @@ impl LocalParticipant { const DATA_BLOB_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); /// Stores an arbitrary blob of data on the server, keyed by `key`. - /// - /// This is a lower-level primitive; most callers should prefer the schema - /// helpers (e.g. [`define_schema`](Self::define_schema)). pub async fn store_data_blob( &self, key: proto::DataBlobKey, @@ -985,34 +982,39 @@ impl LocalParticipant { let session = self.inner.rtc_engine.session(); let request_id = session.signal_client().next_request_id(); + + // Success is reported via `StoreDataBlobResponse` and error via `RequestResponse`; + // both carry the request id, so both paths are correlated by it. + let store_ok_response = session.store_data_blob_response(request_id); + let store_error_response = session.get_response(request_id); + let request = proto::StoreDataBlobRequest { blob: Some(blob), request_id }; self.inner .rtc_engine .send_request(proto::signal_request::Message::StoreDataBlobRequest(request)) .await; - livekit_runtime::spawn(async move { - let Ok(response) = - timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, session.get_response(request_id)).await - else { - return; // No error arrived within the window: treat the store as successful. - }; - if response.reason() != Reason::Ok { - log::error!( - "store data blob failed ({:?}): {}", - response.reason(), - response.message - ); + let response = timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, async { + tokio::select! { + _ = store_ok_response => Ok(()), + error = store_error_response => Err(error), } - }); + }) + .await + .map_err(|_| { + EngineError::Signal(SignalError::Timeout("store data blob timed out".into())) + })?; - Ok(()) + match response { + Ok(()) => Ok(()), + Err(error) => Err(EngineError::Internal( + format!("store data blob request failed ({:?}): {}", error.reason(), error.message) + .into(), + )), + } } /// Retrieves a blob of data previously stored by `participant` under `key`. - /// - /// This is a lower-level primitive; most callers should prefer the schema - /// helpers (e.g. [`get_schema`](Self::get_schema)). pub async fn get_data_blob( &self, key: proto::DataBlobKey, @@ -1020,6 +1022,12 @@ impl LocalParticipant { ) -> EngineResult { let session = self.inner.rtc_engine.session(); let request_id = session.signal_client().next_request_id(); + + // The SFU omits the request id on a successful response, so the success path is + // correlated by the blob key; the error path is correlated by the request id. + let get_ok_response = session.get_data_blob_response(key.clone()); + let get_error_response = session.get_response(request_id); + let request = proto::GetDataBlobRequest { key: Some(key), participant_identity: participant.0, @@ -1032,8 +1040,8 @@ impl LocalParticipant { let response = timeout(Self::DATA_BLOB_REQUEST_TIMEOUT, async { tokio::select! { - response = session.get_data_blob_response(request_id) => Ok(response), - error = session.get_response(request_id) => Err(error), + response = get_ok_response => Ok(response), + error = get_error_response => Err(error), } }) .await diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index a158e33ae..9b1cc8e5b 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -394,7 +394,11 @@ struct SessionInner { negotiation_queue: NegotiationQueue, pending_requests: Mutex>>, - pending_data_blob_requests: Mutex>>, + + pending_store_data_blob_requests: + Mutex>>, + pending_get_data_blob_requests: + Mutex>>, e2ee_manager: Option, subscriber_primary: bool, @@ -608,7 +612,8 @@ impl RtcSession { negotiation_debouncer: Default::default(), negotiation_queue: NegotiationQueue::new(), pending_requests: Default::default(), - pending_data_blob_requests: Default::default(), + pending_get_data_blob_requests: Default::default(), + pending_store_data_blob_requests: Default::default(), e2ee_manager, subscriber_primary, pc_state_notify: Notify::new(), @@ -912,8 +917,17 @@ impl RtcSession { self.inner.get_response(request_id).await } - pub async fn get_data_blob_response(&self, request_id: u32) -> proto::GetDataBlobResponse { - self.inner.get_data_blob_response(request_id).await + /// Awaits the successful [`GetDataBlobResponse`][proto::GetDataBlobResponse] for `key`. + pub async fn get_data_blob_response( + &self, + key: proto::DataBlobKey, + ) -> proto::GetDataBlobResponse { + self.inner.get_data_blob_response(key).await + } + + /// Awaits the successful [`StoreDataBlobResponse`][proto::StoreDataBlobResponse] for `request_id`. + pub async fn store_data_blob_response(&self, request_id: u32) -> proto::StoreDataBlobResponse { + self.inner.store_data_blob_response(request_id).await } } @@ -1339,8 +1353,15 @@ impl SessionInner { } } proto::signal_response::Message::GetDataBlobResponse(response) => { + if let Some(key) = response.blob.as_ref().and_then(|b| b.key.clone()) { + if let Some(tx) = self.pending_get_data_blob_requests.lock().remove(&key) { + let _ = tx.send(response); + } + } + } + proto::signal_response::Message::StoreDataBlobResponse(response) => { if let Some(tx) = - self.pending_data_blob_requests.lock().remove(&response.request_id) + self.pending_store_data_blob_requests.lock().remove(&response.request_id) { let _ = tx.send(response); } @@ -2263,12 +2284,19 @@ impl SessionInner { rx.await.unwrap() } - async fn get_data_blob_response(&self, request_id: u32) -> proto::GetDataBlobResponse { + async fn get_data_blob_response(&self, key: proto::DataBlobKey) -> proto::GetDataBlobResponse { let (tx, rx) = oneshot::channel(); - self.pending_data_blob_requests.lock().insert(request_id, tx); - let _guard = PendingResponseGuard::new(&self.pending_data_blob_requests, request_id); + self.pending_get_data_blob_requests.lock().insert(key.clone(), tx); + let _guard = PendingResponseGuard::new(&self.pending_get_data_blob_requests, key); rx.await.expect("data blob response sender dropped") } + + async fn store_data_blob_response(&self, request_id: u32) -> proto::StoreDataBlobResponse { + let (tx, rx) = oneshot::channel(); + self.pending_store_data_blob_requests.lock().insert(request_id, tx); + let _guard = PendingResponseGuard::new(&self.pending_store_data_blob_requests, request_id); + rx.await.expect("store data blob response sender dropped") + } } /// Removes a pending response registration when dropped. @@ -2276,20 +2304,20 @@ impl SessionInner { /// Installed alongside a registration so that abandoning the wait (e.g. a timeout or a /// losing [`tokio::select!`] branch) cannot leave a stale entry behind. Dropping after the /// response has already been delivered is a no-op since the entry is removed on delivery. -struct PendingResponseGuard<'a, T> { - map: &'a Mutex>>, - request_id: u32, +struct PendingResponseGuard<'a, K: Eq + std::hash::Hash, T> { + map: &'a Mutex>>, + key: K, } -impl<'a, T> PendingResponseGuard<'a, T> { - fn new(map: &'a Mutex>>, request_id: u32) -> Self { - Self { map, request_id } +impl<'a, K: Eq + std::hash::Hash, T> PendingResponseGuard<'a, K, T> { + fn new(map: &'a Mutex>>, key: K) -> Self { + Self { map, key } } } -impl Drop for PendingResponseGuard<'_, T> { +impl Drop for PendingResponseGuard<'_, K, T> { fn drop(&mut self) { - self.map.lock().remove(&self.request_id); + self.map.lock().remove(&self.key); } } diff --git a/livekit/tests/data_blob_test.rs b/livekit/tests/data_blob_test.rs new file mode 100644 index 000000000..96da52f86 --- /dev/null +++ b/livekit/tests/data_blob_test.rs @@ -0,0 +1,94 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[cfg(feature = "__lk-e2e-test")] +use { + anyhow::{Ok, Result}, + bytes::Bytes, + common::test_rooms, + livekit_protocol as proto, +}; + +mod common; + +const MAX_DATA_BLOB_SIZE: usize = 60_000; + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_store_data_blob() -> Result<()> { + let mut rooms = test_rooms(2).await?; + let (pub_room, _) = rooms.pop().unwrap(); + let (sub_room, _) = rooms.pop().unwrap(); + let identity = pub_room.local_participant().identity(); + + let key = data_blob_key("some_key"); + let contents = Bytes::from_static(&[0xFA; MAX_DATA_BLOB_SIZE]); + + pub_room.local_participant().store_data_blob(key.clone(), contents.clone()).await?; + + let definition = sub_room.local_participant().get_data_blob(key, identity).await?; + assert_eq!(definition, contents); + + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_store_data_blob_over_limit() -> Result<()> { + let (room, _) = test_rooms(1).await?.pop().unwrap(); + + let key = data_blob_key("some_key"); + let contents = Bytes::from_static(&[0xFA; 2 * MAX_DATA_BLOB_SIZE]); // Deliberately over size limit + + let result = room.local_participant().store_data_blob(key, contents).await; + assert!(result.is_err()); + + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_store_data_blob_duplicate() -> Result<()> { + let (room, _) = test_rooms(1).await?.pop().unwrap(); + + let key = data_blob_key("some_key"); + let contents = Bytes::from_static(&[0xFA; MAX_DATA_BLOB_SIZE]); + + room.local_participant().store_data_blob(key.clone(), contents.clone()).await?; + + // Store under same key again + let result = room.local_participant().store_data_blob(key, contents).await; + assert!(result.is_err()); + + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_get_data_blob_unknown_key() -> Result<()> { + let (room, _) = test_rooms(1).await?.pop().unwrap(); + let identity = room.local_participant().identity(); + + let key = data_blob_key("unknown_key"); + let result = room.local_participant().get_data_blob(key, identity).await; + + assert!(result.is_err()); + + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +fn data_blob_key(string: &str) -> proto::DataBlobKey { + proto::DataBlobKey { key: Some(proto::data_blob_key::Key::Generic(string.to_string())) } +} diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index a61f726dc..dc2b2f196 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -455,6 +455,8 @@ async fn test_schema_storage() -> Result<()> { pub_room.local_participant().define_schema(schema_id.clone(), DEFINITION.to_string()).await?; + tokio::time::sleep(Duration::from_millis(1000)).await; + let definition = sub_room.local_participant().get_schema(schema_id, pub_identity).await?; assert_eq!(definition, DEFINITION); From e56d21b1de28337ad3e523034051856e8dec76d0 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:39:15 -0700 Subject: [PATCH 22/28] Note to make internal --- livekit/src/room/participant/local_participant.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 2599b36ea..7c1b0c59b 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -972,6 +972,7 @@ impl LocalParticipant { // TODO: unify request/response logic, timeout behavior across SDK. const DATA_BLOB_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); + // TODO: make internal /// Stores an arbitrary blob of data on the server, keyed by `key`. pub async fn store_data_blob( &self, @@ -1014,6 +1015,7 @@ impl LocalParticipant { } } + // TODO: make internal /// Retrieves a blob of data previously stored by `participant` under `key`. pub async fn get_data_blob( &self, From 125a7d40d918349b90f3d0af7ae5241bc24faf58 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 17 Jun 2026 08:41:53 -0700 Subject: [PATCH 23/28] Use request ID --- livekit-protocol/generate_proto.sh | 3 -- livekit-protocol/src/livekit.rs | 3 -- .../src/room/participant/local_participant.rs | 6 +-- livekit/src/rtc_engine/rtc_session.rs | 41 +++++++++---------- 4 files changed, 22 insertions(+), 31 deletions(-) diff --git a/livekit-protocol/generate_proto.sh b/livekit-protocol/generate_proto.sh index 9b8a65ade..e3df93b23 100755 --- a/livekit-protocol/generate_proto.sh +++ b/livekit-protocol/generate_proto.sh @@ -24,9 +24,6 @@ protoc \ --prost_out=$OUT_RUST \ --prost_opt=compile_well_known_types \ --prost_opt=extern_path=.google.protobuf=::pbjson_types \ - '--prost_opt=type_attribute=livekit.DataBlobKey=#[derive(Eq)] #[derive(Hash)]' \ - '--prost_opt=type_attribute=livekit.DataBlobKey.key=#[derive(Eq)] #[derive(Hash)]' \ - '--prost_opt=type_attribute=livekit.DataTrackSchemaId=#[derive(Eq)] #[derive(Hash)]' \ --prost-serde_out=$OUT_RUST \ --prost-serde_opt=ignore_unknown_fields \ $PROTOCOL/livekit_egress.proto \ diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index 95ee01ab4..30a8c0ac8 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -643,7 +643,6 @@ pub struct DataTrackInfo { /// /// Schemas with the same name but different encodings are distinct. /// -#[derive(Eq)] #[derive(Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DataTrackSchemaId { @@ -670,7 +669,6 @@ pub struct DataTrackSubscriptionOptions { pub target_fps: ::core::option::Option, } /// Key used to uniquely identify a data blob for storage and retrieval. -#[derive(Eq)] #[derive(Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DataBlobKey { @@ -679,7 +677,6 @@ pub struct DataBlobKey { } /// Nested message and enum types in `DataBlobKey`. pub mod data_blob_key { - #[derive(Eq)] #[derive(Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Key { diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 7c1b0c59b..2bf118a5b 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -1025,9 +1025,9 @@ impl LocalParticipant { let session = self.inner.rtc_engine.session(); let request_id = session.signal_client().next_request_id(); - // The SFU omits the request id on a successful response, so the success path is - // correlated by the blob key; the error path is correlated by the request id. - let get_ok_response = session.get_data_blob_response(key.clone()); + // Success is reported via `GetDataBlobResponse` and error via `RequestResponse`; + // both carry the request id, so both paths are correlated by it. + let get_ok_response = session.get_data_blob_response(request_id); let get_error_response = session.get_response(request_id); let request = proto::GetDataBlobRequest { diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 9b1cc8e5b..efc2bdcd6 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -398,7 +398,7 @@ struct SessionInner { pending_store_data_blob_requests: Mutex>>, pending_get_data_blob_requests: - Mutex>>, + Mutex>>, e2ee_manager: Option, subscriber_primary: bool, @@ -917,12 +917,9 @@ impl RtcSession { self.inner.get_response(request_id).await } - /// Awaits the successful [`GetDataBlobResponse`][proto::GetDataBlobResponse] for `key`. - pub async fn get_data_blob_response( - &self, - key: proto::DataBlobKey, - ) -> proto::GetDataBlobResponse { - self.inner.get_data_blob_response(key).await + /// Awaits the successful [`GetDataBlobResponse`][proto::GetDataBlobResponse] for `request_id`. + pub async fn get_data_blob_response(&self, request_id: u32) -> proto::GetDataBlobResponse { + self.inner.get_data_blob_response(request_id).await } /// Awaits the successful [`StoreDataBlobResponse`][proto::StoreDataBlobResponse] for `request_id`. @@ -1353,10 +1350,10 @@ impl SessionInner { } } proto::signal_response::Message::GetDataBlobResponse(response) => { - if let Some(key) = response.blob.as_ref().and_then(|b| b.key.clone()) { - if let Some(tx) = self.pending_get_data_blob_requests.lock().remove(&key) { - let _ = tx.send(response); - } + if let Some(tx) = + self.pending_get_data_blob_requests.lock().remove(&response.request_id) + { + let _ = tx.send(response); } } proto::signal_response::Message::StoreDataBlobResponse(response) => { @@ -2284,10 +2281,10 @@ impl SessionInner { rx.await.unwrap() } - async fn get_data_blob_response(&self, key: proto::DataBlobKey) -> proto::GetDataBlobResponse { + async fn get_data_blob_response(&self, request_id: u32) -> proto::GetDataBlobResponse { let (tx, rx) = oneshot::channel(); - self.pending_get_data_blob_requests.lock().insert(key.clone(), tx); - let _guard = PendingResponseGuard::new(&self.pending_get_data_blob_requests, key); + self.pending_get_data_blob_requests.lock().insert(request_id, tx); + let _guard = PendingResponseGuard::new(&self.pending_get_data_blob_requests, request_id); rx.await.expect("data blob response sender dropped") } @@ -2304,20 +2301,20 @@ impl SessionInner { /// Installed alongside a registration so that abandoning the wait (e.g. a timeout or a /// losing [`tokio::select!`] branch) cannot leave a stale entry behind. Dropping after the /// response has already been delivered is a no-op since the entry is removed on delivery. -struct PendingResponseGuard<'a, K: Eq + std::hash::Hash, T> { - map: &'a Mutex>>, - key: K, +struct PendingResponseGuard<'a, T> { + map: &'a Mutex>>, + request_id: u32, } -impl<'a, K: Eq + std::hash::Hash, T> PendingResponseGuard<'a, K, T> { - fn new(map: &'a Mutex>>, key: K) -> Self { - Self { map, key } +impl<'a, T> PendingResponseGuard<'a, T> { + fn new(map: &'a Mutex>>, request_id: u32) -> Self { + Self { map, request_id } } } -impl Drop for PendingResponseGuard<'_, K, T> { +impl Drop for PendingResponseGuard<'_, T> { fn drop(&mut self) { - self.map.lock().remove(&self.key); + self.map.lock().remove(&self.request_id); } } From a3b39caba953b9b4fe9c83e07bf6ec7e1cbd2799 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:57:36 -0700 Subject: [PATCH 24/28] Make data blobs private, test schema storage only --- .../src/room/participant/local_participant.rs | 16 +++--- livekit/tests/data_track_test.rs | 39 -------------- ...ta_blob_test.rs => schema_storage_test.rs} | 51 ++++++++----------- 3 files changed, 29 insertions(+), 77 deletions(-) rename livekit/tests/{data_blob_test.rs => schema_storage_test.rs} (50%) diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 2bf118a5b..03999a6e7 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -961,7 +961,11 @@ impl LocalParticipant { let contents = self .get_data_blob(id.into(), participant) .await - .inspect_err(|err| log::error!("Failed to get schema: {err}"))?; + .map_err(|err| { + log::error!("failed to get schema: {err}"); + err + })?; + let definition = String::from_utf8(contents.to_vec()).map_err(|err| { RoomError::Internal(format!("schema definition is not valid UTF-8: {err}")) @@ -972,13 +976,8 @@ impl LocalParticipant { // TODO: unify request/response logic, timeout behavior across SDK. const DATA_BLOB_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); - // TODO: make internal /// Stores an arbitrary blob of data on the server, keyed by `key`. - pub async fn store_data_blob( - &self, - key: proto::DataBlobKey, - contents: Bytes, - ) -> EngineResult<()> { + async fn store_data_blob(&self, key: proto::DataBlobKey, contents: Bytes) -> EngineResult<()> { let blob = proto::DataBlob { key: Some(key), contents: contents.into() }; let session = self.inner.rtc_engine.session(); @@ -1015,9 +1014,8 @@ impl LocalParticipant { } } - // TODO: make internal /// Retrieves a blob of data previously stored by `participant` under `key`. - pub async fn get_data_blob( + async fn get_data_blob( &self, key: proto::DataBlobKey, participant: ParticipantIdentity, diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index dc2b2f196..6b7930deb 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -439,45 +439,6 @@ async fn test_publisher_side_fault(scenario: SimulateScenario) -> Result<()> { Ok(()) } -#[cfg(feature = "__lk-e2e-test")] -#[test_log::test(tokio::test)] -async fn test_schema_storage() -> Result<()> { - use livekit::data_track::{DataTrackSchemaEncoding, DataTrackSchemaId}; - - const DEFINITION: &str = "my schema definition"; - - let mut rooms = test_rooms(2).await?; - let (pub_room, _) = rooms.pop().unwrap(); - let (sub_room, _) = rooms.pop().unwrap(); - let pub_identity = pub_room.local_participant().identity(); - - let schema_id = DataTrackSchemaId::new("my_schema", DataTrackSchemaEncoding::JsonSchema); - - pub_room.local_participant().define_schema(schema_id.clone(), DEFINITION.to_string()).await?; - - tokio::time::sleep(Duration::from_millis(1000)).await; - - let definition = sub_room.local_participant().get_schema(schema_id, pub_identity).await?; - assert_eq!(definition, DEFINITION); - - Ok(()) -} - -#[cfg(feature = "__lk-e2e-test")] -#[test_log::test(tokio::test)] -async fn test_get_undefined_schema() -> Result<()> { - use livekit::data_track::{DataTrackSchemaEncoding, DataTrackSchemaId}; - - let (room, _) = test_rooms(1).await?.pop().unwrap(); - let identity = room.local_participant().identity(); - - let schema_id = DataTrackSchemaId::new("missing_schema", DataTrackSchemaEncoding::JsonSchema); - let result = room.local_participant().get_schema(schema_id, identity).await; - assert!(result.is_err()); - - Ok(()) -} - /// Waits for the first remote data track to be published. #[cfg(feature = "__lk-e2e-test")] async fn wait_for_remote_track( diff --git a/livekit/tests/data_blob_test.rs b/livekit/tests/schema_storage_test.rs similarity index 50% rename from livekit/tests/data_blob_test.rs rename to livekit/tests/schema_storage_test.rs index 96da52f86..3629077d2 100644 --- a/livekit/tests/data_blob_test.rs +++ b/livekit/tests/schema_storage_test.rs @@ -15,43 +15,42 @@ #[cfg(feature = "__lk-e2e-test")] use { anyhow::{Ok, Result}, - bytes::Bytes, common::test_rooms, - livekit_protocol as proto, + livekit::data_track::{DataTrackSchemaEncoding, DataTrackSchemaId}, }; mod common; -const MAX_DATA_BLOB_SIZE: usize = 60_000; +const MAX_SCHEMA_DEFINITION_SIZE: usize = 60_000; #[cfg(feature = "__lk-e2e-test")] #[test_log::test(tokio::test)] -async fn test_store_data_blob() -> Result<()> { +async fn test_define_schema() -> Result<()> { let mut rooms = test_rooms(2).await?; let (pub_room, _) = rooms.pop().unwrap(); let (sub_room, _) = rooms.pop().unwrap(); let identity = pub_room.local_participant().identity(); - let key = data_blob_key("some_key"); - let contents = Bytes::from_static(&[0xFA; MAX_DATA_BLOB_SIZE]); + let id = DataTrackSchemaId::new("some_schema", DataTrackSchemaEncoding::JsonSchema); + let definition = "a".repeat(MAX_SCHEMA_DEFINITION_SIZE); - pub_room.local_participant().store_data_blob(key.clone(), contents.clone()).await?; + pub_room.local_participant().define_schema(id.clone(), definition.clone()).await?; - let definition = sub_room.local_participant().get_data_blob(key, identity).await?; - assert_eq!(definition, contents); + let retrieved = sub_room.local_participant().get_schema(id, identity).await?; + assert_eq!(retrieved, definition); Ok(()) } #[cfg(feature = "__lk-e2e-test")] #[test_log::test(tokio::test)] -async fn test_store_data_blob_over_limit() -> Result<()> { +async fn test_define_schema_over_limit() -> Result<()> { let (room, _) = test_rooms(1).await?.pop().unwrap(); - let key = data_blob_key("some_key"); - let contents = Bytes::from_static(&[0xFA; 2 * MAX_DATA_BLOB_SIZE]); // Deliberately over size limit + let id = DataTrackSchemaId::new("some_schema", DataTrackSchemaEncoding::JsonSchema); + let definition = "a".repeat(2 * MAX_SCHEMA_DEFINITION_SIZE); // Deliberately over size limit - let result = room.local_participant().store_data_blob(key, contents).await; + let result = room.local_participant().define_schema(id, definition).await; assert!(result.is_err()); Ok(()) @@ -59,16 +58,16 @@ async fn test_store_data_blob_over_limit() -> Result<()> { #[cfg(feature = "__lk-e2e-test")] #[test_log::test(tokio::test)] -async fn test_store_data_blob_duplicate() -> Result<()> { +async fn test_define_schema_duplicate() -> Result<()> { let (room, _) = test_rooms(1).await?.pop().unwrap(); - let key = data_blob_key("some_key"); - let contents = Bytes::from_static(&[0xFA; MAX_DATA_BLOB_SIZE]); + let id = DataTrackSchemaId::new("some_schema", DataTrackSchemaEncoding::JsonSchema); + let definition = "a".repeat(MAX_SCHEMA_DEFINITION_SIZE); - room.local_participant().store_data_blob(key.clone(), contents.clone()).await?; + room.local_participant().define_schema(id.clone(), definition.clone()).await?; - // Store under same key again - let result = room.local_participant().store_data_blob(key, contents).await; + // Define the same schema again + let result = room.local_participant().define_schema(id, definition).await; assert!(result.is_err()); Ok(()) @@ -76,19 +75,13 @@ async fn test_store_data_blob_duplicate() -> Result<()> { #[cfg(feature = "__lk-e2e-test")] #[test_log::test(tokio::test)] -async fn test_get_data_blob_unknown_key() -> Result<()> { +async fn test_get_undefined_schema() -> Result<()> { let (room, _) = test_rooms(1).await?.pop().unwrap(); let identity = room.local_participant().identity(); - let key = data_blob_key("unknown_key"); - let result = room.local_participant().get_data_blob(key, identity).await; - + let id = DataTrackSchemaId::new("undefined", DataTrackSchemaEncoding::JsonSchema); + let result = room.local_participant().get_schema(id, identity).await; assert!(result.is_err()); Ok(()) -} - -#[cfg(feature = "__lk-e2e-test")] -fn data_blob_key(string: &str) -> proto::DataBlobKey { - proto::DataBlobKey { key: Some(proto::data_blob_key::Key::Generic(string.to_string())) } -} +} \ No newline at end of file From 33baaf70dc1d6a43c0b76cd4ab0fa0f33610078d Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:05:06 -0700 Subject: [PATCH 25/28] Add accessors for schema and frame encoding --- livekit-datatrack/src/track.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index 59e92a8ec..e8d95b495 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -97,6 +97,24 @@ impl DataTrackInfo { pub fn uses_e2ee(&self) -> bool { self.uses_e2ee } + + /// Schema associated with frames sent on the track. + /// + /// Returns `None` if the publisher did not associate a + /// [`DataTrackSchemaId`] with the track. + /// + pub fn schema(&self) -> Option<&DataTrackSchemaId> { + self.schema.as_ref() + } + + /// Encoding of frames sent on the track. + /// + /// Returns `None` if the publisher did not specify a + /// [`DataTrackFrameEncoding`] for the track. + /// + pub fn frame_encoding(&self) -> Option { + self.frame_encoding + } } /// SFU-assigned identifier uniquely identifying a data track. From 18381f2cd8759d6f4dfa404699afe339fdea52e4 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:05:28 -0700 Subject: [PATCH 26/28] Test publish with metadata --- livekit/tests/data_track_test.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 6b7930deb..a23b152ea 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -160,6 +160,35 @@ async fn test_publish_duplicate_name() -> Result<()> { Ok(()) } +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_publish_with_schema_and_frame_encoding() -> Result<()> { + use livekit::data_track::{DataTrackFrameEncoding, DataTrackSchemaEncoding, DataTrackSchemaId}; + + let mut rooms = test_rooms(2).await?; + let (pub_room, _) = rooms.pop().unwrap(); + let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); + + let schema_id = DataTrackSchemaId::new("my_schema", DataTrackSchemaEncoding::JsonSchema); + let frame_encoding = DataTrackFrameEncoding::Json; + + let options = DataTrackOptions::new("my_track") + .with_schema(schema_id.clone()) + .with_frame_encoding(frame_encoding); + + let local_track = pub_room.local_participant().publish_data_track(options).await?; + assert_eq!(local_track.info().schema(), Some(&schema_id)); + assert_eq!(local_track.info().frame_encoding(), Some(frame_encoding)); + + // The subscriber should observe the same schema and frame encoding metadata. + let remote_track = + timeout(Duration::from_secs(5), wait_for_remote_track(&mut sub_room_event_rx)).await??; + assert_eq!(remote_track.info().schema(), Some(&schema_id)); + assert_eq!(remote_track.info().frame_encoding(), Some(frame_encoding)); + + Ok(()) +} + #[cfg(feature = "__lk-e2e-test")] #[test_log::test(tokio::test)] async fn test_e2ee() -> Result<()> { From 62bf4bbe6a0776c68ecc5f75221c3c74b80b8690 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:09:40 -0700 Subject: [PATCH 27/28] Don't log error --- livekit/src/room/participant/local_participant.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 03999a6e7..9dd32aa0f 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -961,11 +961,7 @@ impl LocalParticipant { let contents = self .get_data_blob(id.into(), participant) .await - .map_err(|err| { - log::error!("failed to get schema: {err}"); - err - })?; - + .map_err(|err| RoomError::Internal(format!("failed to fetch schema: {err}")))?; let definition = String::from_utf8(contents.to_vec()).map_err(|err| { RoomError::Internal(format!("schema definition is not valid UTF-8: {err}")) From a271c9d558c655b7d12a545929ce015230c122c8 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:25:08 -0700 Subject: [PATCH 28/28] Expose data track fields over FFI --- livekit-ffi/protocol/data_track.proto | 36 ++++++++++++---- livekit-ffi/src/conversion/data_track.rs | 53 ++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/livekit-ffi/protocol/data_track.proto b/livekit-ffi/protocol/data_track.proto index ce034f410..c5c55f220 100644 --- a/livekit-ffi/protocol/data_track.proto +++ b/livekit-ffi/protocol/data_track.proto @@ -30,6 +30,10 @@ message DataTrackInfo { required string sid = 2; // Whether or not frames sent on the track use end-to-end encryption. required bool uses_e2ee = 3; + // Schema associated with frames sent on the track, if any. + optional DataTrackSchemaId schema = 4; + // Encoding of frames sent on the track, if specified. + optional DataTrackFrameEncoding frame_encoding = 5; } // A frame published on a data track. @@ -110,6 +114,10 @@ message DataTrackOptions { // Must not be empty and must be unique per publisher. // required string name = 1; + // Schema describing frames sent on the track. + optional DataTrackSchemaId schema = 2; + // Encoding of frames sent on the track. + optional DataTrackFrameEncoding frame_encoding = 3; } // Publish a data track @@ -251,14 +259,26 @@ message DataTrackStreamEOS { // Encoding used to interpret a data track schema definition. enum DataTrackSchemaEncoding { - DATA_TRACK_SCHEMA_ENCODING_PROTOBUF = 0; - DATA_TRACK_SCHEMA_ENCODING_FLATBUFFER = 1; - DATA_TRACK_SCHEMA_ENCODING_ROS1_MSG = 2; - DATA_TRACK_SCHEMA_ENCODING_ROS2_MSG = 3; - DATA_TRACK_SCHEMA_ENCODING_ROS2_IDL = 4; - DATA_TRACK_SCHEMA_ENCODING_OMG_IDL = 5; - DATA_TRACK_SCHEMA_ENCODING_JSON_SCHEMA = 6; - DATA_TRACK_SCHEMA_ENCODING_OTHER = 7; + DATA_TRACK_SCHEMA_ENCODING_OTHER = 0; + DATA_TRACK_SCHEMA_ENCODING_PROTOBUF = 1; + DATA_TRACK_SCHEMA_ENCODING_FLATBUFFER = 2; + DATA_TRACK_SCHEMA_ENCODING_ROS1_MSG = 3; + DATA_TRACK_SCHEMA_ENCODING_ROS2_MSG = 4; + DATA_TRACK_SCHEMA_ENCODING_ROS2_IDL = 5; + DATA_TRACK_SCHEMA_ENCODING_OMG_IDL = 6; + DATA_TRACK_SCHEMA_ENCODING_JSON_SCHEMA = 7; +} + +// Encoding used for frames sent on a data track. +enum DataTrackFrameEncoding { + DATA_TRACK_FRAME_ENCODING_OTHER = 0; + DATA_TRACK_FRAME_ENCODING_ROS1 = 1; + DATA_TRACK_FRAME_ENCODING_CDR = 2; + DATA_TRACK_FRAME_ENCODING_PROTOBUF = 3; + DATA_TRACK_FRAME_ENCODING_FLATBUFFER = 4; + DATA_TRACK_FRAME_ENCODING_CBOR = 5; + DATA_TRACK_FRAME_ENCODING_MSGPACK = 6; + DATA_TRACK_FRAME_ENCODING_JSON = 7; } // Uniquely identifies a data track schema. diff --git a/livekit-ffi/src/conversion/data_track.rs b/livekit-ffi/src/conversion/data_track.rs index 7b668905b..db4f41726 100644 --- a/livekit-ffi/src/conversion/data_track.rs +++ b/livekit-ffi/src/conversion/data_track.rs @@ -15,16 +15,24 @@ use crate::proto; use livekit::{ data_track::{ - DataTrackFrame, DataTrackInfo, DataTrackOptions, DataTrackSchemaEncoding, - DataTrackSchemaId, DataTrackSubscribeError, PublishError, PushFrameError, - PushFrameErrorReason, RemoteDataTrackPipelineOptions, + DataTrackFrame, DataTrackFrameEncoding, DataTrackInfo, DataTrackOptions, + DataTrackSchemaEncoding, DataTrackSchemaId, DataTrackSubscribeError, PublishError, + PushFrameError, PushFrameErrorReason, RemoteDataTrackPipelineOptions, }, prelude::DataTrackSubscribeOptions, }; impl From for DataTrackOptions { fn from(options: proto::DataTrackOptions) -> Self { - Self::new(options.name) + let frame_encoding = options.frame_encoding.map(|_| options.frame_encoding().into()); + let mut result = Self::new(options.name); + if let Some(schema) = options.schema { + result = result.with_schema(schema.into()); + } + if let Some(frame_encoding) = frame_encoding { + result = result.with_frame_encoding(frame_encoding); + } + result } } @@ -34,6 +42,10 @@ impl From for proto::DataTrackInfo { name: info.name().to_string(), sid: info.sid().to_string(), uses_e2ee: info.uses_e2ee(), + schema: info.schema().cloned().map(Into::into), + frame_encoding: info + .frame_encoding() + .map(|encoding| proto::DataTrackFrameEncoding::from(encoding) as i32), } } } @@ -107,6 +119,39 @@ impl From for proto::DataTrackSchemaEncoding { } } +impl From for DataTrackFrameEncoding { + fn from(encoding: proto::DataTrackFrameEncoding) -> Self { + match encoding { + proto::DataTrackFrameEncoding::Ros1 => Self::Ros1, + proto::DataTrackFrameEncoding::Cdr => Self::Cdr, + proto::DataTrackFrameEncoding::Protobuf => Self::Protobuf, + proto::DataTrackFrameEncoding::Flatbuffer => Self::Flatbuffer, + proto::DataTrackFrameEncoding::Cbor => Self::Cbor, + proto::DataTrackFrameEncoding::Msgpack => Self::Msgpack, + proto::DataTrackFrameEncoding::Json => Self::Json, + proto::DataTrackFrameEncoding::Other => Self::Other, + } + } +} + +impl From for proto::DataTrackFrameEncoding { + fn from(encoding: DataTrackFrameEncoding) -> Self { + match encoding { + DataTrackFrameEncoding::Ros1 => Self::Ros1, + DataTrackFrameEncoding::Cdr => Self::Cdr, + DataTrackFrameEncoding::Protobuf => Self::Protobuf, + DataTrackFrameEncoding::Flatbuffer => Self::Flatbuffer, + DataTrackFrameEncoding::Cbor => Self::Cbor, + DataTrackFrameEncoding::Msgpack => Self::Msgpack, + DataTrackFrameEncoding::Json => Self::Json, + DataTrackFrameEncoding::Other => Self::Other, + // `DataTrackFrameEncoding` is `#[non_exhaustive]`; map any future + // variant to the catch-all encoding. + _ => Self::Other, + } + } +} + impl From for DataTrackSchemaId { fn from(msg: proto::DataTrackSchemaId) -> Self { let encoding = msg.encoding().into();