From 83783a46316d7d7a1cb1bbd71b1868a6a08c307a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 11 Oct 2025 14:05:36 +1100 Subject: [PATCH 001/232] Use WIP protocol for data tracks --- livekit-protocol/protocol | 2 +- livekit-protocol/src/livekit.rs | 246 ++- livekit-protocol/src/livekit.serde.rs | 2104 ++++++++++++++++++++++--- 3 files changed, 2154 insertions(+), 198 deletions(-) diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index 2bc93ddc2..c357756e3 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit 2bc93ddc27ccfa66ee8d270a1bcd115586fb601d +Subproject commit c357756e39c7b23364e328cdd1619483ef5e68ef diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index 676becb62..6248bd7eb 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -12,7 +12,7 @@ pub struct MetricsBatch { /// This is useful for storing participant identities, track names, etc. /// There is also a predefined list of labels that can be used to reference common metrics. /// They have reserved indices from 0 to (METRIC_LABEL_PREDEFINED_MAX_VALUE - 1). - /// Indexes pointing at str_data should start from METRIC_LABEL_PREDEFINED_MAX_VALUE, + /// Indexes pointing at str_data should start from METRIC_LABEL_PREDEFINED_MAX_VALUE, /// such that str_data\[0\] == index of METRIC_LABEL_PREDEFINED_MAX_VALUE. #[prost(string, repeated, tag="3")] pub str_data: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, @@ -78,6 +78,14 @@ pub struct EventMetric { #[prost(uint32, tag="9")] pub rid: u32, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MetricsRecordingHeader { + #[prost(string, tag="1")] + pub room_id: ::prost::alloc::string::String, + #[prost(bool, optional, tag="2")] + pub enable_user_data_training: ::core::option::Option, +} // // Protocol used to record metrics for a specific session. // @@ -217,7 +225,7 @@ pub struct ListUpdate { pub add: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, /// delete items from a list #[prost(string, repeated, tag="3")] - pub del: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + pub remove: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, /// sets the list to an empty list #[prost(bool, tag="4")] pub clear: bool, @@ -346,6 +354,9 @@ pub struct ParticipantInfo { pub disconnect_reason: i32, #[prost(enumeration="participant_info::KindDetail", repeated, tag="18")] pub kind_details: ::prost::alloc::vec::Vec, + /// NEXT_ID: 20 + #[prost(message, repeated, tag="19")] + pub data_tracks: ::prost::alloc::vec::Vec, } /// Nested message and enum types in `ParticipantInfo`. pub mod participant_info { @@ -521,11 +532,9 @@ pub struct TrackInfo { pub muted: bool, /// original width of video (unset for audio) /// clients may receive a lower resolution version with simulcast - #[deprecated] #[prost(uint32, tag="5")] pub width: u32, /// original height of video (unset for audio) - #[deprecated] #[prost(uint32, tag="6")] pub height: u32, /// true if track is simulcasted @@ -572,6 +581,34 @@ pub struct TrackInfo { #[prost(enumeration="BackupCodecPolicy", tag="20")] pub backup_codec_policy: i32, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackInfo { + #[prost(string, tag="1")] + pub sid: ::prost::alloc::string::String, + /// 16-bit, SFU-assigned identifier used to associate packets with the track, unique per room. + #[prost(uint32, tag="2")] + pub handle: u32, + /// Human-readable identifier (e.g., `geoLocation`, `servoPosition.x`, etc.), unique per publisher. + #[prost(string, tag="3")] + pub name: ::prost::alloc::string::String, + /// MIME type of the data sent over the track (e.g., `application/json`). + /// This must be a valid MIME type as defined by RFC 2046. + #[prost(string, tag="4")] + pub mime_type: ::prost::alloc::string::String, + /// Method used for end-to-end encryption (E2EE) on packet payloads. + #[prost(enumeration="encryption::Type", tag="5")] + pub encryption: i32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackSubscribeOptions { + /// Rate in frames per second (FPS) the subscriber wants to receive frames at. + /// If omitted, the subscriber defaults to the publisher's nominal FPS; if the + /// publisher has no nominal FPS, it will use the maximum. + #[prost(uint32, optional, tag="1")] + pub target_fps: ::core::option::Option, +} /// provide information about available spatial layers #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -601,6 +638,7 @@ pub mod video_layer { Unused = 0, OneSpatialLayerPerStream = 1, MultipleSpatialLayersPerStream = 2, + OneSpatialLayerPerStreamIncompleteRtcpSr = 3, } impl Mode { /// String value of the enum field names used in the ProtoBuf definition. @@ -612,6 +650,7 @@ pub mod video_layer { Mode::Unused => "MODE_UNUSED", Mode::OneSpatialLayerPerStream => "ONE_SPATIAL_LAYER_PER_STREAM", Mode::MultipleSpatialLayersPerStream => "MULTIPLE_SPATIAL_LAYERS_PER_STREAM", + Mode::OneSpatialLayerPerStreamIncompleteRtcpSr => "ONE_SPATIAL_LAYER_PER_STREAM_INCOMPLETE_RTCP_SR", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -620,6 +659,7 @@ pub mod video_layer { "MODE_UNUSED" => Some(Self::Unused), "ONE_SPATIAL_LAYER_PER_STREAM" => Some(Self::OneSpatialLayerPerStream), "MULTIPLE_SPATIAL_LAYERS_PER_STREAM" => Some(Self::MultipleSpatialLayersPerStream), + "ONE_SPATIAL_LAYER_PER_STREAM_INCOMPLETE_RTCP_SR" => Some(Self::OneSpatialLayerPerStreamIncompleteRtcpSr), _ => None, } } @@ -715,7 +755,7 @@ pub struct EncryptedPacket { pub iv: ::prost::alloc::vec::Vec, #[prost(uint32, tag="3")] pub key_index: u32, - /// This is an encrypted EncryptedPacketPayload message representation + /// This is an encrypted EncryptedPacketPayload message representation #[prost(bytes="vec", tag="4")] pub encrypted_value: ::prost::alloc::vec::Vec, } @@ -1440,11 +1480,29 @@ pub mod data_stream { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct FilterParams { + #[prost(string, repeated, tag="1")] + pub include_events: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(string, repeated, tag="2")] + pub exclude_events: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct WebhookConfig { #[prost(string, tag="1")] pub url: ::prost::alloc::string::String, #[prost(string, tag="2")] pub signing_key: ::prost::alloc::string::String, + #[prost(message, optional, tag="3")] + pub filter_params: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribedAudioCodec { + #[prost(string, tag="1")] + pub codec: ::prost::alloc::string::String, + #[prost(bool, tag="2")] + pub enabled: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] @@ -1452,6 +1510,7 @@ pub enum AudioCodec { DefaultAc = 0, Opus = 1, Aac = 2, + AcMp3 = 3, } impl AudioCodec { /// String value of the enum field names used in the ProtoBuf definition. @@ -1463,6 +1522,7 @@ impl AudioCodec { AudioCodec::DefaultAc => "DEFAULT_AC", AudioCodec::Opus => "OPUS", AudioCodec::Aac => "AAC", + AudioCodec::AcMp3 => "AC_MP3", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -1471,6 +1531,7 @@ impl AudioCodec { "DEFAULT_AC" => Some(Self::DefaultAc), "OPUS" => Some(Self::Opus), "AAC" => Some(Self::Aac), + "AC_MP3" => Some(Self::AcMp3), _ => None, } } @@ -2703,6 +2764,7 @@ pub enum EncodedFileType { DefaultFiletype = 0, Mp4 = 1, Ogg = 2, + Mp3 = 3, } impl EncodedFileType { /// String value of the enum field names used in the ProtoBuf definition. @@ -2714,6 +2776,7 @@ impl EncodedFileType { EncodedFileType::DefaultFiletype => "DEFAULT_FILETYPE", EncodedFileType::Mp4 => "MP4", EncodedFileType::Ogg => "OGG", + EncodedFileType::Mp3 => "MP3", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -2722,6 +2785,7 @@ impl EncodedFileType { "DEFAULT_FILETYPE" => Some(Self::DefaultFiletype), "MP4" => Some(Self::Mp4), "OGG" => Some(Self::Ogg), + "MP3" => Some(Self::Mp3), _ => None, } } @@ -2992,7 +3056,7 @@ impl EgressSourceType { #[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")] + #[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")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalRequest`. @@ -3051,12 +3115,18 @@ pub mod signal_request { /// Update local video track settings #[prost(message, tag="18")] UpdateVideoTrack(super::UpdateLocalVideoTrack), + /// Publish a data track + #[prost(message, tag="19")] + AddDataTrack(super::AddDataTrackRequest), + /// Update subscription state for one or more data tracks + #[prost(message, tag="20")] + UpdateDataSubscription(super::UpdateDataSubscription), } } #[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")] + #[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")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalResponse`. @@ -3139,6 +3209,12 @@ pub mod signal_response { /// notify number of required media sections to satisfy subscribed tracks #[prost(message, tag="25")] MediaSectionsRequirement(super::MediaSectionsRequirement), + /// when audio subscription changes, used to enable simulcasting of audio codecs based on subscriptions + #[prost(message, tag="26")] + SubscribedAudioCodecUpdate(super::SubscribedAudioCodecUpdate), + /// Sent in response to `AddDataTrackRequest`. + #[prost(message, tag="27")] + DataTrackPublished(super::DataTrackPublishedResponse), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -3205,6 +3281,77 @@ pub struct AddTrackRequest { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct AddDataTrackRequest { + /// Human-readable identifier (e.g., `geoLocation`, `servoPosition.x`, etc.), unique per publisher. + #[prost(string, tag="1")] + pub name: ::prost::alloc::string::String, + /// MIME type of the data sent over the track (e.g., `application/json`). + /// This must be a valid MIME type as defined by RFC 2046. + #[prost(string, tag="2")] + pub mime_type: ::prost::alloc::string::String, + /// Method used for end-to-end encryption (E2EE) on frame payloads. + #[prost(enumeration="encryption::Type", tag="3")] + pub encryption: i32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackPublishedResponse { + #[prost(uint32, tag="1")] + pub request_id: u32, + #[prost(oneof="data_track_published_response::Result", tags="2, 3")] + pub result: ::core::option::Option, +} +/// Nested message and enum types in `DataTrackPublishedResponse`. +pub mod data_track_published_response { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum Error { + /// Unknown reason. + Unknown = 0, + /// Name is invalid. + InvalidName = 1, + /// MIME type is invalid. + InvalidMimeType = 2, + /// Publisher already has a data track published with the same name. + NameTaken = 3, + } + impl Error { + /// 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 { + Error::Unknown => "UNKNOWN", + Error::InvalidName => "INVALID_NAME", + Error::InvalidMimeType => "INVALID_MIME_TYPE", + Error::NameTaken => "NAME_TAKEN", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "UNKNOWN" => Some(Self::Unknown), + "INVALID_NAME" => Some(Self::InvalidName), + "INVALID_MIME_TYPE" => Some(Self::InvalidMimeType), + "NAME_TAKEN" => Some(Self::NameTaken), + _ => None, + } + } + } + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Result { + /// Track was successfully published, track info is provided. + #[prost(message, tag="2")] + Ok(super::DataTrackInfo), + /// Track could not be published, reason is provided. + #[prost(enumeration="Error", tag="3")] + Error(i32), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct TrickleRequest { #[prost(string, tag="1")] pub candidate_init: ::prost::alloc::string::String, @@ -3318,6 +3465,18 @@ pub struct UpdateSubscription { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct UpdateDataSubscription { + #[prost(string, repeated, tag="1")] + pub track_sids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(bool, tag="2")] + pub subscribe: bool, + /// Options for each track subscription. Entries in this list align + /// positionally with `track_sids`. + #[prost(message, repeated, tag="3")] + pub options: ::prost::alloc::vec::Vec, +} +#[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>, @@ -3520,6 +3679,14 @@ pub struct SubscribedQualityUpdate { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribedAudioCodecUpdate { + #[prost(string, tag="1")] + pub track_sid: ::prost::alloc::string::String, + #[prost(message, repeated, tag="2")] + pub subscribed_audio_codecs: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct TrackPermission { /// permission could be granted either by participant sid or identity #[prost(string, tag="1")] @@ -3697,6 +3864,8 @@ 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")] + pub request: ::core::option::Option, } /// Nested message and enum types in `RequestResponse`. pub mod request_response { @@ -3707,6 +3876,9 @@ pub mod request_response { NotFound = 1, NotAllowed = 2, LimitExceeded = 3, + Queued = 4, + UnsupportedType = 5, + UnclassifiedError = 6, } impl Reason { /// String value of the enum field names used in the ProtoBuf definition. @@ -3719,6 +3891,9 @@ pub mod request_response { Reason::NotFound => "NOT_FOUND", Reason::NotAllowed => "NOT_ALLOWED", Reason::LimitExceeded => "LIMIT_EXCEEDED", + Reason::Queued => "QUEUED", + Reason::UnsupportedType => "UNSUPPORTED_TYPE", + Reason::UnclassifiedError => "UNCLASSIFIED_ERROR", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -3728,10 +3903,29 @@ pub mod request_response { "NOT_FOUND" => Some(Self::NotFound), "NOT_ALLOWED" => Some(Self::NotAllowed), "LIMIT_EXCEEDED" => Some(Self::LimitExceeded), + "QUEUED" => Some(Self::Queued), + "UNSUPPORTED_TYPE" => Some(Self::UnsupportedType), + "UNCLASSIFIED_ERROR" => Some(Self::UnclassifiedError), _ => None, } } } + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Request { + #[prost(message, tag="4")] + Trickle(super::TrickleRequest), + #[prost(message, tag="5")] + AddTrack(super::AddTrackRequest), + #[prost(message, tag="6")] + Mute(super::MuteTrackRequest), + #[prost(message, tag="7")] + UpdateMetadata(super::UpdateParticipantMetadata), + #[prost(message, tag="8")] + UpdateAudioTrack(super::UpdateLocalAudioTrack), + #[prost(message, tag="9")] + UpdateVideoTrack(super::UpdateLocalVideoTrack), + } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3928,6 +4122,8 @@ pub struct Job { pub agent_name: ::prost::alloc::string::String, #[prost(message, optional, tag="8")] pub state: ::core::option::Option, + #[prost(bool, tag="10")] + pub enable_recording: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -4571,6 +4767,26 @@ pub struct MoveParticipantResponse { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct PerformRpcRequest { + #[prost(string, tag="1")] + pub room: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub destination_identity: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub method: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub payload: ::prost::alloc::string::String, + #[prost(uint32, tag="5")] + pub response_timeout_ms: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PerformRpcResponse { + #[prost(string, tag="1")] + pub payload: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct CreateIngressRequest { #[prost(enumeration="IngressInput", tag="1")] pub input_type: i32, @@ -5715,10 +5931,18 @@ pub struct CreateSipParticipantRequest { #[prost(enumeration="SipMediaEncryption", tag="18")] pub media_encryption: i32, /// Wait for the answer for the call before returning. - /// - /// NEXT ID: 21 #[prost(bool, tag="19")] pub wait_until_answered: bool, + /// Optional display name for the 'From' SIP header. + /// + /// Cases: + /// 1) Unspecified: Use legacy behavior - display name will be set to be the caller's number. + /// 2) Empty string: Do not send a display name, which will result in a CNAM lookup downstream. + /// 3) Non-empty: Use the specified value as the display name. + /// + /// NEXT ID: 22 + #[prost(string, optional, tag="21")] + pub display_name: ::core::option::Option<::prost::alloc::string::String>, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -5806,6 +6030,10 @@ pub struct SipCallInfo { pub audio_codec: ::prost::alloc::string::String, #[prost(string, tag="21")] pub media_encryption: ::prost::alloc::string::String, + #[prost(string, tag="25")] + pub pcap_file_link: ::prost::alloc::string::String, + #[prost(message, repeated, tag="26")] + pub call_context: ::prost::alloc::vec::Vec<::pbjson_types::Any>, } #[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 3c79f6c7a..44b89de26 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -94,6 +94,138 @@ impl<'de> serde::Deserialize<'de> for ActiveSpeakerUpdate { deserializer.deserialize_struct("livekit.ActiveSpeakerUpdate", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for AddDataTrackRequest { + #[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.mime_type.is_empty() { + len += 1; + } + if self.encryption != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.AddDataTrackRequest", len)?; + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + if !self.mime_type.is_empty() { + struct_ser.serialize_field("mimeType", &self.mime_type)?; + } + if self.encryption != 0 { + let v = encryption::Type::try_from(self.encryption) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; + struct_ser.serialize_field("encryption", &v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AddDataTrackRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "name", + "mime_type", + "mimeType", + "encryption", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Name, + MimeType, + Encryption, + __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), + "mimeType" | "mime_type" => Ok(GeneratedField::MimeType), + "encryption" => Ok(GeneratedField::Encryption), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AddDataTrackRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.AddDataTrackRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut name__ = None; + let mut mime_type__ = None; + let mut encryption__ = 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::MimeType => { + if mime_type__.is_some() { + return Err(serde::de::Error::duplicate_field("mimeType")); + } + mime_type__ = Some(map_.next_value()?); + } + GeneratedField::Encryption => { + if encryption__.is_some() { + return Err(serde::de::Error::duplicate_field("encryption")); + } + encryption__ = Some(map_.next_value::()? as i32); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(AddDataTrackRequest { + name: name__.unwrap_or_default(), + mime_type: mime_type__.unwrap_or_default(), + encryption: encryption__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.AddDataTrackRequest", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for AddTrackRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -959,6 +1091,7 @@ impl serde::Serialize for AudioCodec { Self::DefaultAc => "DEFAULT_AC", Self::Opus => "OPUS", Self::Aac => "AAC", + Self::AcMp3 => "AC_MP3", }; serializer.serialize_str(variant) } @@ -973,6 +1106,7 @@ impl<'de> serde::Deserialize<'de> for AudioCodec { "DEFAULT_AC", "OPUS", "AAC", + "AC_MP3", ]; struct GeneratedVisitor; @@ -1016,6 +1150,7 @@ impl<'de> serde::Deserialize<'de> for AudioCodec { "DEFAULT_AC" => Ok(AudioCodec::DefaultAc), "OPUS" => Ok(AudioCodec::Opus), "AAC" => Ok(AudioCodec::Aac), + "AC_MP3" => Ok(AudioCodec::AcMp3), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -4759,6 +4894,9 @@ impl serde::Serialize for CreateSipParticipantRequest { if self.wait_until_answered { len += 1; } + if self.display_name.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.CreateSIPParticipantRequest", len)?; if !self.sip_trunk_id.is_empty() { struct_ser.serialize_field("sipTrunkId", &self.sip_trunk_id)?; @@ -4824,6 +4962,9 @@ impl serde::Serialize for CreateSipParticipantRequest { if self.wait_until_answered { struct_ser.serialize_field("waitUntilAnswered", &self.wait_until_answered)?; } + if let Some(v) = self.display_name.as_ref() { + struct_ser.serialize_field("displayName", v)?; + } struct_ser.end() } } @@ -4871,6 +5012,8 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { "mediaEncryption", "wait_until_answered", "waitUntilAnswered", + "display_name", + "displayName", ]; #[allow(clippy::enum_variant_names)] @@ -4895,6 +5038,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { KrispEnabled, MediaEncryption, WaitUntilAnswered, + DisplayName, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -4937,6 +5081,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { "krispEnabled" | "krisp_enabled" => Ok(GeneratedField::KrispEnabled), "mediaEncryption" | "media_encryption" => Ok(GeneratedField::MediaEncryption), "waitUntilAnswered" | "wait_until_answered" => Ok(GeneratedField::WaitUntilAnswered), + "displayName" | "display_name" => Ok(GeneratedField::DisplayName), _ => Ok(GeneratedField::__SkipField__), } } @@ -4976,6 +5121,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { let mut krisp_enabled__ = None; let mut media_encryption__ = None; let mut wait_until_answered__ = None; + let mut display_name__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::SipTrunkId => { @@ -5102,6 +5248,12 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { } wait_until_answered__ = Some(map_.next_value()?); } + GeneratedField::DisplayName => { + if display_name__.is_some() { + return Err(serde::de::Error::duplicate_field("displayName")); + } + display_name__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -5128,6 +5280,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { krisp_enabled: krisp_enabled__.unwrap_or_default(), media_encryption: media_encryption__.unwrap_or_default(), wait_until_answered: wait_until_answered__.unwrap_or_default(), + display_name: display_name__, }) } } @@ -7060,7 +7213,7 @@ impl<'de> serde::Deserialize<'de> for data_stream::Trailer { deserializer.deserialize_struct("livekit.DataStream.Trailer", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DeleteAgentDispatchRequest { +impl serde::Serialize for DataTrackInfo { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -7068,38 +7221,64 @@ impl serde::Serialize for DeleteAgentDispatchRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.dispatch_id.is_empty() { + if !self.sid.is_empty() { len += 1; } - if !self.room.is_empty() { + if self.handle != 0 { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.DeleteAgentDispatchRequest", len)?; - if !self.dispatch_id.is_empty() { - struct_ser.serialize_field("dispatchId", &self.dispatch_id)?; + if !self.name.is_empty() { + len += 1; } - if !self.room.is_empty() { - struct_ser.serialize_field("room", &self.room)?; + if !self.mime_type.is_empty() { + len += 1; + } + if self.encryption != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackInfo", len)?; + if !self.sid.is_empty() { + struct_ser.serialize_field("sid", &self.sid)?; + } + if self.handle != 0 { + struct_ser.serialize_field("handle", &self.handle)?; + } + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + if !self.mime_type.is_empty() { + struct_ser.serialize_field("mimeType", &self.mime_type)?; + } + if self.encryption != 0 { + let v = encryption::Type::try_from(self.encryption) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; + struct_ser.serialize_field("encryption", &v)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DeleteAgentDispatchRequest { +impl<'de> serde::Deserialize<'de> for DataTrackInfo { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "dispatch_id", - "dispatchId", - "room", + "sid", + "handle", + "name", + "mime_type", + "mimeType", + "encryption", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - DispatchId, - Room, + Sid, + Handle, + Name, + MimeType, + Encryption, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -7122,8 +7301,11 @@ impl<'de> serde::Deserialize<'de> for DeleteAgentDispatchRequest { E: serde::de::Error, { match value { - "dispatchId" | "dispatch_id" => Ok(GeneratedField::DispatchId), - "room" => Ok(GeneratedField::Room), + "sid" => Ok(GeneratedField::Sid), + "handle" => Ok(GeneratedField::Handle), + "name" => Ok(GeneratedField::Name), + "mimeType" | "mime_type" => Ok(GeneratedField::MimeType), + "encryption" => Ok(GeneratedField::Encryption), _ => Ok(GeneratedField::__SkipField__), } } @@ -7133,47 +7315,73 @@ impl<'de> serde::Deserialize<'de> for DeleteAgentDispatchRequest { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DeleteAgentDispatchRequest; + type Value = DataTrackInfo; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DeleteAgentDispatchRequest") + formatter.write_str("struct livekit.DataTrackInfo") } - 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 dispatch_id__ = None; - let mut room__ = None; + let mut sid__ = None; + let mut handle__ = None; + let mut name__ = None; + let mut mime_type__ = None; + let mut encryption__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::DispatchId => { - if dispatch_id__.is_some() { - return Err(serde::de::Error::duplicate_field("dispatchId")); + GeneratedField::Sid => { + if sid__.is_some() { + return Err(serde::de::Error::duplicate_field("sid")); } - dispatch_id__ = Some(map_.next_value()?); + sid__ = Some(map_.next_value()?); } - GeneratedField::Room => { - if room__.is_some() { - return Err(serde::de::Error::duplicate_field("room")); + GeneratedField::Handle => { + if handle__.is_some() { + return Err(serde::de::Error::duplicate_field("handle")); } - room__ = Some(map_.next_value()?); + handle__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Name => { + if name__.is_some() { + return Err(serde::de::Error::duplicate_field("name")); + } + name__ = Some(map_.next_value()?); + } + GeneratedField::MimeType => { + if mime_type__.is_some() { + return Err(serde::de::Error::duplicate_field("mimeType")); + } + mime_type__ = Some(map_.next_value()?); + } + GeneratedField::Encryption => { + if encryption__.is_some() { + return Err(serde::de::Error::duplicate_field("encryption")); + } + encryption__ = Some(map_.next_value::()? as i32); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(DeleteAgentDispatchRequest { - dispatch_id: dispatch_id__.unwrap_or_default(), - room: room__.unwrap_or_default(), + Ok(DataTrackInfo { + sid: sid__.unwrap_or_default(), + handle: handle__.unwrap_or_default(), + name: name__.unwrap_or_default(), + mime_type: mime_type__.unwrap_or_default(), + encryption: encryption__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.DeleteAgentDispatchRequest", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DataTrackInfo", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DeleteIngressRequest { +impl serde::Serialize for DataTrackPublishedResponse { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -7181,30 +7389,452 @@ impl serde::Serialize for DeleteIngressRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.ingress_id.is_empty() { + if self.request_id != 0 { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.DeleteIngressRequest", len)?; - if !self.ingress_id.is_empty() { - struct_ser.serialize_field("ingressId", &self.ingress_id)?; + if self.result.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackPublishedResponse", len)?; + if self.request_id != 0 { + struct_ser.serialize_field("requestId", &self.request_id)?; + } + if let Some(v) = self.result.as_ref() { + match v { + data_track_published_response::Result::Ok(v) => { + struct_ser.serialize_field("ok", v)?; + } + data_track_published_response::Result::Error(v) => { + let v = data_track_published_response::Error::try_from(*v) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", *v)))?; + struct_ser.serialize_field("error", &v)?; + } + } } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DeleteIngressRequest { +impl<'de> serde::Deserialize<'de> for DataTrackPublishedResponse { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "ingress_id", - "ingressId", + "request_id", + "requestId", + "ok", + "error", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - IngressId, + RequestId, + Ok, + Error, + __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), + "ok" => Ok(GeneratedField::Ok), + "error" => Ok(GeneratedField::Error), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackPublishedResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackPublishedResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut request_id__ = None; + let mut result__ = 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::Ok => { + if result__.is_some() { + return Err(serde::de::Error::duplicate_field("ok")); + } + result__ = map_.next_value::<::std::option::Option<_>>()?.map(data_track_published_response::Result::Ok) +; + } + GeneratedField::Error => { + if result__.is_some() { + return Err(serde::de::Error::duplicate_field("error")); + } + result__ = map_.next_value::<::std::option::Option>()?.map(|x| data_track_published_response::Result::Error(x as i32)); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataTrackPublishedResponse { + request_id: request_id__.unwrap_or_default(), + result: result__, + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackPublishedResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for data_track_published_response::Error { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Unknown => "UNKNOWN", + Self::InvalidName => "INVALID_NAME", + Self::InvalidMimeType => "INVALID_MIME_TYPE", + Self::NameTaken => "NAME_TAKEN", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for data_track_published_response::Error { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "UNKNOWN", + "INVALID_NAME", + "INVALID_MIME_TYPE", + "NAME_TAKEN", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = data_track_published_response::Error; + + 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 { + "UNKNOWN" => Ok(data_track_published_response::Error::Unknown), + "INVALID_NAME" => Ok(data_track_published_response::Error::InvalidName), + "INVALID_MIME_TYPE" => Ok(data_track_published_response::Error::InvalidMimeType), + "NAME_TAKEN" => Ok(data_track_published_response::Error::NameTaken), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for DataTrackSubscribeOptions { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.target_fps.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSubscribeOptions", len)?; + if let Some(v) = self.target_fps.as_ref() { + struct_ser.serialize_field("targetFps", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataTrackSubscribeOptions { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "target_fps", + "targetFps", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + TargetFps, + __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 { + "targetFps" | "target_fps" => Ok(GeneratedField::TargetFps), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackSubscribeOptions; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackSubscribeOptions") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut target_fps__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::TargetFps => { + if target_fps__.is_some() { + return Err(serde::de::Error::duplicate_field("targetFps")); + } + target_fps__ = + map_.next_value::<::std::option::Option<::pbjson::private::NumberDeserialize<_>>>()?.map(|x| x.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataTrackSubscribeOptions { + target_fps: target_fps__, + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackSubscribeOptions", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DeleteAgentDispatchRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.dispatch_id.is_empty() { + len += 1; + } + if !self.room.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DeleteAgentDispatchRequest", len)?; + if !self.dispatch_id.is_empty() { + struct_ser.serialize_field("dispatchId", &self.dispatch_id)?; + } + if !self.room.is_empty() { + struct_ser.serialize_field("room", &self.room)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DeleteAgentDispatchRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "dispatch_id", + "dispatchId", + "room", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + DispatchId, + Room, + __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 { + "dispatchId" | "dispatch_id" => Ok(GeneratedField::DispatchId), + "room" => Ok(GeneratedField::Room), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DeleteAgentDispatchRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DeleteAgentDispatchRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut dispatch_id__ = None; + let mut room__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::DispatchId => { + if dispatch_id__.is_some() { + return Err(serde::de::Error::duplicate_field("dispatchId")); + } + dispatch_id__ = Some(map_.next_value()?); + } + GeneratedField::Room => { + if room__.is_some() { + return Err(serde::de::Error::duplicate_field("room")); + } + room__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DeleteAgentDispatchRequest { + dispatch_id: dispatch_id__.unwrap_or_default(), + room: room__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DeleteAgentDispatchRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DeleteIngressRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.ingress_id.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DeleteIngressRequest", len)?; + if !self.ingress_id.is_empty() { + struct_ser.serialize_field("ingressId", &self.ingress_id)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DeleteIngressRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "ingress_id", + "ingressId", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + IngressId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -8911,6 +9541,7 @@ impl serde::Serialize for EncodedFileType { Self::DefaultFiletype => "DEFAULT_FILETYPE", Self::Mp4 => "MP4", Self::Ogg => "OGG", + Self::Mp3 => "MP3", }; serializer.serialize_str(variant) } @@ -8925,6 +9556,7 @@ impl<'de> serde::Deserialize<'de> for EncodedFileType { "DEFAULT_FILETYPE", "MP4", "OGG", + "MP3", ]; struct GeneratedVisitor; @@ -8968,6 +9600,7 @@ impl<'de> serde::Deserialize<'de> for EncodedFileType { "DEFAULT_FILETYPE" => Ok(EncodedFileType::DefaultFiletype), "MP4" => Ok(EncodedFileType::Mp4), "OGG" => Ok(EncodedFileType::Ogg), + "MP3" => Ok(EncodedFileType::Mp3), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -10334,6 +10967,120 @@ impl<'de> serde::Deserialize<'de> for FileInfo { deserializer.deserialize_struct("livekit.FileInfo", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for FilterParams { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.include_events.is_empty() { + len += 1; + } + if !self.exclude_events.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.FilterParams", len)?; + if !self.include_events.is_empty() { + struct_ser.serialize_field("includeEvents", &self.include_events)?; + } + if !self.exclude_events.is_empty() { + struct_ser.serialize_field("excludeEvents", &self.exclude_events)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for FilterParams { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "include_events", + "includeEvents", + "exclude_events", + "excludeEvents", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + IncludeEvents, + ExcludeEvents, + __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 { + "includeEvents" | "include_events" => Ok(GeneratedField::IncludeEvents), + "excludeEvents" | "exclude_events" => Ok(GeneratedField::ExcludeEvents), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = FilterParams; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.FilterParams") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut include_events__ = None; + let mut exclude_events__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::IncludeEvents => { + if include_events__.is_some() { + return Err(serde::de::Error::duplicate_field("includeEvents")); + } + include_events__ = Some(map_.next_value()?); + } + GeneratedField::ExcludeEvents => { + if exclude_events__.is_some() { + return Err(serde::de::Error::duplicate_field("excludeEvents")); + } + exclude_events__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(FilterParams { + include_events: include_events__.unwrap_or_default(), + exclude_events: exclude_events__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.FilterParams", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for ForwardParticipantRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -13656,6 +14403,9 @@ impl serde::Serialize for Job { if self.state.is_some() { len += 1; } + if self.enable_recording { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.Job", len)?; if !self.id.is_empty() { struct_ser.serialize_field("id", &self.id)?; @@ -13686,6 +14436,9 @@ impl serde::Serialize for Job { if let Some(v) = self.state.as_ref() { struct_ser.serialize_field("state", v)?; } + if self.enable_recording { + struct_ser.serialize_field("enableRecording", &self.enable_recording)?; + } struct_ser.end() } } @@ -13707,6 +14460,8 @@ impl<'de> serde::Deserialize<'de> for Job { "agent_name", "agentName", "state", + "enable_recording", + "enableRecording", ]; #[allow(clippy::enum_variant_names)] @@ -13720,6 +14475,7 @@ impl<'de> serde::Deserialize<'de> for Job { Metadata, AgentName, State, + EnableRecording, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -13751,6 +14507,7 @@ impl<'de> serde::Deserialize<'de> for Job { "metadata" => Ok(GeneratedField::Metadata), "agentName" | "agent_name" => Ok(GeneratedField::AgentName), "state" => Ok(GeneratedField::State), + "enableRecording" | "enable_recording" => Ok(GeneratedField::EnableRecording), _ => Ok(GeneratedField::__SkipField__), } } @@ -13779,6 +14536,7 @@ impl<'de> serde::Deserialize<'de> for Job { let mut metadata__ = None; let mut agent_name__ = None; let mut state__ = None; + let mut enable_recording__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Id => { @@ -13835,6 +14593,12 @@ impl<'de> serde::Deserialize<'de> for Job { } state__ = map_.next_value()?; } + GeneratedField::EnableRecording => { + if enable_recording__.is_some() { + return Err(serde::de::Error::duplicate_field("enableRecording")); + } + enable_recording__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -13850,6 +14614,7 @@ impl<'de> serde::Deserialize<'de> for Job { metadata: metadata__.unwrap_or_default(), agent_name: agent_name__.unwrap_or_default(), state: state__, + enable_recording: enable_recording__.unwrap_or_default(), }) } } @@ -17209,7 +17974,7 @@ impl serde::Serialize for ListUpdate { if !self.add.is_empty() { len += 1; } - if !self.del.is_empty() { + if !self.remove.is_empty() { len += 1; } if self.clear { @@ -17222,8 +17987,8 @@ impl serde::Serialize for ListUpdate { if !self.add.is_empty() { struct_ser.serialize_field("add", &self.add)?; } - if !self.del.is_empty() { - struct_ser.serialize_field("del", &self.del)?; + if !self.remove.is_empty() { + struct_ser.serialize_field("remove", &self.remove)?; } if self.clear { struct_ser.serialize_field("clear", &self.clear)?; @@ -17240,7 +18005,7 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { const FIELDS: &[&str] = &[ "set", "add", - "del", + "remove", "clear", ]; @@ -17248,7 +18013,7 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { enum GeneratedField { Set, Add, - Del, + Remove, Clear, __SkipField__, } @@ -17274,7 +18039,7 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { match value { "set" => Ok(GeneratedField::Set), "add" => Ok(GeneratedField::Add), - "del" => Ok(GeneratedField::Del), + "remove" => Ok(GeneratedField::Remove), "clear" => Ok(GeneratedField::Clear), _ => Ok(GeneratedField::__SkipField__), } @@ -17297,7 +18062,7 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { { let mut set__ = None; let mut add__ = None; - let mut del__ = None; + let mut remove__ = None; let mut clear__ = None; while let Some(k) = map_.next_key()? { match k { @@ -17313,11 +18078,11 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { } add__ = Some(map_.next_value()?); } - GeneratedField::Del => { - if del__.is_some() { - return Err(serde::de::Error::duplicate_field("del")); + GeneratedField::Remove => { + if remove__.is_some() { + return Err(serde::de::Error::duplicate_field("remove")); } - del__ = Some(map_.next_value()?); + remove__ = Some(map_.next_value()?); } GeneratedField::Clear => { if clear__.is_some() { @@ -17333,7 +18098,7 @@ impl<'de> serde::Deserialize<'de> for ListUpdate { Ok(ListUpdate { set: set__.unwrap_or_default(), add: add__.unwrap_or_default(), - del: del__.unwrap_or_default(), + remove: remove__.unwrap_or_default(), clear: clear__.unwrap_or_default(), }) } @@ -17815,11 +18580,151 @@ impl<'de> serde::Deserialize<'de> for MetricsBatch { E: serde::de::Error, { match value { - "timestampMs" | "timestamp_ms" => Ok(GeneratedField::TimestampMs), - "normalizedTimestamp" | "normalized_timestamp" => Ok(GeneratedField::NormalizedTimestamp), - "strData" | "str_data" => Ok(GeneratedField::StrData), - "timeSeries" | "time_series" => Ok(GeneratedField::TimeSeries), - "events" => Ok(GeneratedField::Events), + "timestampMs" | "timestamp_ms" => Ok(GeneratedField::TimestampMs), + "normalizedTimestamp" | "normalized_timestamp" => Ok(GeneratedField::NormalizedTimestamp), + "strData" | "str_data" => Ok(GeneratedField::StrData), + "timeSeries" | "time_series" => Ok(GeneratedField::TimeSeries), + "events" => Ok(GeneratedField::Events), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = MetricsBatch; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.MetricsBatch") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut timestamp_ms__ = None; + let mut normalized_timestamp__ = None; + let mut str_data__ = None; + let mut time_series__ = None; + let mut events__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::TimestampMs => { + if timestamp_ms__.is_some() { + return Err(serde::de::Error::duplicate_field("timestampMs")); + } + timestamp_ms__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::NormalizedTimestamp => { + if normalized_timestamp__.is_some() { + return Err(serde::de::Error::duplicate_field("normalizedTimestamp")); + } + normalized_timestamp__ = map_.next_value()?; + } + GeneratedField::StrData => { + if str_data__.is_some() { + return Err(serde::de::Error::duplicate_field("strData")); + } + str_data__ = Some(map_.next_value()?); + } + GeneratedField::TimeSeries => { + if time_series__.is_some() { + return Err(serde::de::Error::duplicate_field("timeSeries")); + } + time_series__ = Some(map_.next_value()?); + } + GeneratedField::Events => { + if events__.is_some() { + return Err(serde::de::Error::duplicate_field("events")); + } + events__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(MetricsBatch { + timestamp_ms: timestamp_ms__.unwrap_or_default(), + normalized_timestamp: normalized_timestamp__, + str_data: str_data__.unwrap_or_default(), + time_series: time_series__.unwrap_or_default(), + events: events__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.MetricsBatch", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for MetricsRecordingHeader { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.room_id.is_empty() { + len += 1; + } + if self.enable_user_data_training.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.MetricsRecordingHeader", len)?; + if !self.room_id.is_empty() { + struct_ser.serialize_field("roomId", &self.room_id)?; + } + if let Some(v) = self.enable_user_data_training.as_ref() { + struct_ser.serialize_field("enableUserDataTraining", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "room_id", + "roomId", + "enable_user_data_training", + "enableUserDataTraining", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + RoomId, + EnableUserDataTraining, + __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 { + "roomId" | "room_id" => Ok(GeneratedField::RoomId), + "enableUserDataTraining" | "enable_user_data_training" => Ok(GeneratedField::EnableUserDataTraining), _ => Ok(GeneratedField::__SkipField__), } } @@ -17829,70 +18734,44 @@ impl<'de> serde::Deserialize<'de> for MetricsBatch { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = MetricsBatch; + type Value = MetricsRecordingHeader; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.MetricsBatch") + formatter.write_str("struct livekit.MetricsRecordingHeader") } - 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 timestamp_ms__ = None; - let mut normalized_timestamp__ = None; - let mut str_data__ = None; - let mut time_series__ = None; - let mut events__ = None; + let mut room_id__ = None; + let mut enable_user_data_training__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::TimestampMs => { - if timestamp_ms__.is_some() { - return Err(serde::de::Error::duplicate_field("timestampMs")); - } - timestamp_ms__ = - Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) - ; - } - GeneratedField::NormalizedTimestamp => { - if normalized_timestamp__.is_some() { - return Err(serde::de::Error::duplicate_field("normalizedTimestamp")); - } - normalized_timestamp__ = map_.next_value()?; - } - GeneratedField::StrData => { - if str_data__.is_some() { - return Err(serde::de::Error::duplicate_field("strData")); - } - str_data__ = Some(map_.next_value()?); - } - GeneratedField::TimeSeries => { - if time_series__.is_some() { - return Err(serde::de::Error::duplicate_field("timeSeries")); + GeneratedField::RoomId => { + if room_id__.is_some() { + return Err(serde::de::Error::duplicate_field("roomId")); } - time_series__ = Some(map_.next_value()?); + room_id__ = Some(map_.next_value()?); } - GeneratedField::Events => { - if events__.is_some() { - return Err(serde::de::Error::duplicate_field("events")); + GeneratedField::EnableUserDataTraining => { + if enable_user_data_training__.is_some() { + return Err(serde::de::Error::duplicate_field("enableUserDataTraining")); } - events__ = Some(map_.next_value()?); + enable_user_data_training__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(MetricsBatch { - timestamp_ms: timestamp_ms__.unwrap_or_default(), - normalized_timestamp: normalized_timestamp__, - str_data: str_data__.unwrap_or_default(), - time_series: time_series__.unwrap_or_default(), - events: events__.unwrap_or_default(), + Ok(MetricsRecordingHeader { + room_id: room_id__.unwrap_or_default(), + enable_user_data_training: enable_user_data_training__, }) } } - deserializer.deserialize_struct("livekit.MetricsBatch", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.MetricsRecordingHeader", FIELDS, GeneratedVisitor) } } impl serde::Serialize for MigrateJobRequest { @@ -18974,6 +19853,9 @@ impl serde::Serialize for ParticipantInfo { if !self.kind_details.is_empty() { len += 1; } + if !self.data_tracks.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.ParticipantInfo", len)?; if !self.sid.is_empty() { struct_ser.serialize_field("sid", &self.sid)?; @@ -19037,6 +19919,9 @@ impl serde::Serialize for ParticipantInfo { }).collect::, _>>()?; struct_ser.serialize_field("kindDetails", &v)?; } + if !self.data_tracks.is_empty() { + struct_ser.serialize_field("dataTracks", &self.data_tracks)?; + } struct_ser.end() } } @@ -19068,6 +19953,8 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { "disconnectReason", "kind_details", "kindDetails", + "data_tracks", + "dataTracks", ]; #[allow(clippy::enum_variant_names)] @@ -19088,6 +19975,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { Attributes, DisconnectReason, KindDetails, + DataTracks, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -19126,6 +20014,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { "attributes" => Ok(GeneratedField::Attributes), "disconnectReason" | "disconnect_reason" => Ok(GeneratedField::DisconnectReason), "kindDetails" | "kind_details" => Ok(GeneratedField::KindDetails), + "dataTracks" | "data_tracks" => Ok(GeneratedField::DataTracks), _ => Ok(GeneratedField::__SkipField__), } } @@ -19161,6 +20050,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { let mut attributes__ = None; let mut disconnect_reason__ = None; let mut kind_details__ = None; + let mut data_tracks__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Sid => { @@ -19267,6 +20157,12 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { } kind_details__ = Some(map_.next_value::>()?.into_iter().map(|x| x as i32).collect()); } + GeneratedField::DataTracks => { + if data_tracks__.is_some() { + return Err(serde::de::Error::duplicate_field("dataTracks")); + } + data_tracks__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -19289,6 +20185,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { attributes: attributes__.unwrap_or_default(), disconnect_reason: disconnect_reason__.unwrap_or_default(), kind_details: kind_details__.unwrap_or_default(), + data_tracks: data_tracks__.unwrap_or_default(), }) } } @@ -19827,8 +20724,244 @@ impl<'de> serde::Deserialize<'de> for ParticipantTracks { E: serde::de::Error, { match value { - "participantSid" | "participant_sid" => Ok(GeneratedField::ParticipantSid), - "trackSids" | "track_sids" => Ok(GeneratedField::TrackSids), + "participantSid" | "participant_sid" => Ok(GeneratedField::ParticipantSid), + "trackSids" | "track_sids" => Ok(GeneratedField::TrackSids), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ParticipantTracks; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.ParticipantTracks") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut participant_sid__ = None; + let mut track_sids__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::ParticipantSid => { + if participant_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("participantSid")); + } + participant_sid__ = Some(map_.next_value()?); + } + GeneratedField::TrackSids => { + if track_sids__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSids")); + } + track_sids__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(ParticipantTracks { + participant_sid: participant_sid__.unwrap_or_default(), + track_sids: track_sids__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.ParticipantTracks", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ParticipantUpdate { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.participants.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.ParticipantUpdate", len)?; + if !self.participants.is_empty() { + struct_ser.serialize_field("participants", &self.participants)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ParticipantUpdate { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "participants", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Participants, + __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 { + "participants" => Ok(GeneratedField::Participants), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ParticipantUpdate; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.ParticipantUpdate") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut participants__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Participants => { + if participants__.is_some() { + return Err(serde::de::Error::duplicate_field("participants")); + } + participants__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(ParticipantUpdate { + participants: participants__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.ParticipantUpdate", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for PerformRpcRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.room.is_empty() { + len += 1; + } + if !self.destination_identity.is_empty() { + len += 1; + } + if !self.method.is_empty() { + len += 1; + } + if !self.payload.is_empty() { + len += 1; + } + if self.response_timeout_ms != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.PerformRpcRequest", len)?; + if !self.room.is_empty() { + struct_ser.serialize_field("room", &self.room)?; + } + if !self.destination_identity.is_empty() { + struct_ser.serialize_field("destinationIdentity", &self.destination_identity)?; + } + if !self.method.is_empty() { + struct_ser.serialize_field("method", &self.method)?; + } + if !self.payload.is_empty() { + struct_ser.serialize_field("payload", &self.payload)?; + } + if self.response_timeout_ms != 0 { + struct_ser.serialize_field("responseTimeoutMs", &self.response_timeout_ms)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for PerformRpcRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "room", + "destination_identity", + "destinationIdentity", + "method", + "payload", + "response_timeout_ms", + "responseTimeoutMs", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Room, + DestinationIdentity, + Method, + Payload, + ResponseTimeoutMs, + __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 { + "room" => Ok(GeneratedField::Room), + "destinationIdentity" | "destination_identity" => Ok(GeneratedField::DestinationIdentity), + "method" => Ok(GeneratedField::Method), + "payload" => Ok(GeneratedField::Payload), + "responseTimeoutMs" | "response_timeout_ms" => Ok(GeneratedField::ResponseTimeoutMs), _ => Ok(GeneratedField::__SkipField__), } } @@ -19838,47 +20971,73 @@ impl<'de> serde::Deserialize<'de> for ParticipantTracks { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = ParticipantTracks; + type Value = PerformRpcRequest; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.ParticipantTracks") + formatter.write_str("struct livekit.PerformRpcRequest") } - 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 participant_sid__ = None; - let mut track_sids__ = None; + let mut room__ = None; + let mut destination_identity__ = None; + let mut method__ = None; + let mut payload__ = None; + let mut response_timeout_ms__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::ParticipantSid => { - if participant_sid__.is_some() { - return Err(serde::de::Error::duplicate_field("participantSid")); + GeneratedField::Room => { + if room__.is_some() { + return Err(serde::de::Error::duplicate_field("room")); } - participant_sid__ = Some(map_.next_value()?); + room__ = Some(map_.next_value()?); } - GeneratedField::TrackSids => { - if track_sids__.is_some() { - return Err(serde::de::Error::duplicate_field("trackSids")); + GeneratedField::DestinationIdentity => { + if destination_identity__.is_some() { + return Err(serde::de::Error::duplicate_field("destinationIdentity")); } - track_sids__ = Some(map_.next_value()?); + destination_identity__ = Some(map_.next_value()?); + } + GeneratedField::Method => { + if method__.is_some() { + return Err(serde::de::Error::duplicate_field("method")); + } + method__ = Some(map_.next_value()?); + } + GeneratedField::Payload => { + if payload__.is_some() { + return Err(serde::de::Error::duplicate_field("payload")); + } + payload__ = Some(map_.next_value()?); + } + GeneratedField::ResponseTimeoutMs => { + if response_timeout_ms__.is_some() { + return Err(serde::de::Error::duplicate_field("responseTimeoutMs")); + } + response_timeout_ms__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(ParticipantTracks { - participant_sid: participant_sid__.unwrap_or_default(), - track_sids: track_sids__.unwrap_or_default(), + Ok(PerformRpcRequest { + room: room__.unwrap_or_default(), + destination_identity: destination_identity__.unwrap_or_default(), + method: method__.unwrap_or_default(), + payload: payload__.unwrap_or_default(), + response_timeout_ms: response_timeout_ms__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.ParticipantTracks", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.PerformRpcRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for ParticipantUpdate { +impl serde::Serialize for PerformRpcResponse { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -19886,29 +21045,29 @@ impl serde::Serialize for ParticipantUpdate { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.participants.is_empty() { + if !self.payload.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.ParticipantUpdate", len)?; - if !self.participants.is_empty() { - struct_ser.serialize_field("participants", &self.participants)?; + let mut struct_ser = serializer.serialize_struct("livekit.PerformRpcResponse", len)?; + if !self.payload.is_empty() { + struct_ser.serialize_field("payload", &self.payload)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for ParticipantUpdate { +impl<'de> serde::Deserialize<'de> for PerformRpcResponse { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "participants", + "payload", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Participants, + Payload, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -19931,7 +21090,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantUpdate { E: serde::de::Error, { match value { - "participants" => Ok(GeneratedField::Participants), + "payload" => Ok(GeneratedField::Payload), _ => Ok(GeneratedField::__SkipField__), } } @@ -19941,36 +21100,36 @@ impl<'de> serde::Deserialize<'de> for ParticipantUpdate { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = ParticipantUpdate; + type Value = PerformRpcResponse; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.ParticipantUpdate") + formatter.write_str("struct livekit.PerformRpcResponse") } - 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 participants__ = None; + let mut payload__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Participants => { - if participants__.is_some() { - return Err(serde::de::Error::duplicate_field("participants")); + GeneratedField::Payload => { + if payload__.is_some() { + return Err(serde::de::Error::duplicate_field("payload")); } - participants__ = Some(map_.next_value()?); + payload__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(ParticipantUpdate { - participants: participants__.unwrap_or_default(), + Ok(PerformRpcResponse { + payload: payload__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.ParticipantUpdate", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.PerformRpcResponse", FIELDS, GeneratedVisitor) } } impl serde::Serialize for Ping { @@ -23217,6 +24376,9 @@ impl serde::Serialize for RequestResponse { if !self.message.is_empty() { len += 1; } + if self.request.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.RequestResponse", len)?; if self.request_id != 0 { struct_ser.serialize_field("requestId", &self.request_id)?; @@ -23229,6 +24391,28 @@ impl serde::Serialize for RequestResponse { if !self.message.is_empty() { struct_ser.serialize_field("message", &self.message)?; } + if let Some(v) = self.request.as_ref() { + match v { + request_response::Request::Trickle(v) => { + struct_ser.serialize_field("trickle", v)?; + } + request_response::Request::AddTrack(v) => { + struct_ser.serialize_field("addTrack", v)?; + } + request_response::Request::Mute(v) => { + struct_ser.serialize_field("mute", v)?; + } + request_response::Request::UpdateMetadata(v) => { + struct_ser.serialize_field("updateMetadata", v)?; + } + request_response::Request::UpdateAudioTrack(v) => { + struct_ser.serialize_field("updateAudioTrack", v)?; + } + request_response::Request::UpdateVideoTrack(v) => { + struct_ser.serialize_field("updateVideoTrack", v)?; + } + } + } struct_ser.end() } } @@ -23243,6 +24427,16 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "requestId", "reason", "message", + "trickle", + "add_track", + "addTrack", + "mute", + "update_metadata", + "updateMetadata", + "update_audio_track", + "updateAudioTrack", + "update_video_track", + "updateVideoTrack", ]; #[allow(clippy::enum_variant_names)] @@ -23250,6 +24444,12 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { RequestId, Reason, Message, + Trickle, + AddTrack, + Mute, + UpdateMetadata, + UpdateAudioTrack, + UpdateVideoTrack, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -23275,6 +24475,12 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "requestId" | "request_id" => Ok(GeneratedField::RequestId), "reason" => Ok(GeneratedField::Reason), "message" => Ok(GeneratedField::Message), + "trickle" => Ok(GeneratedField::Trickle), + "addTrack" | "add_track" => Ok(GeneratedField::AddTrack), + "mute" => Ok(GeneratedField::Mute), + "updateMetadata" | "update_metadata" => Ok(GeneratedField::UpdateMetadata), + "updateAudioTrack" | "update_audio_track" => Ok(GeneratedField::UpdateAudioTrack), + "updateVideoTrack" | "update_video_track" => Ok(GeneratedField::UpdateVideoTrack), _ => Ok(GeneratedField::__SkipField__), } } @@ -23297,6 +24503,7 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { let mut request_id__ = None; let mut reason__ = None; let mut message__ = None; + let mut request__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::RequestId => { @@ -23319,6 +24526,48 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { } message__ = Some(map_.next_value()?); } + GeneratedField::Trickle => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("trickle")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::Trickle) +; + } + GeneratedField::AddTrack => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("addTrack")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::AddTrack) +; + } + GeneratedField::Mute => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("mute")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::Mute) +; + } + GeneratedField::UpdateMetadata => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("updateMetadata")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UpdateMetadata) +; + } + GeneratedField::UpdateAudioTrack => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("updateAudioTrack")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UpdateAudioTrack) +; + } + GeneratedField::UpdateVideoTrack => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("updateVideoTrack")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UpdateVideoTrack) +; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -23328,6 +24577,7 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { request_id: request_id__.unwrap_or_default(), reason: reason__.unwrap_or_default(), message: message__.unwrap_or_default(), + request: request__, }) } } @@ -23345,6 +24595,9 @@ impl serde::Serialize for request_response::Reason { Self::NotFound => "NOT_FOUND", Self::NotAllowed => "NOT_ALLOWED", Self::LimitExceeded => "LIMIT_EXCEEDED", + Self::Queued => "QUEUED", + Self::UnsupportedType => "UNSUPPORTED_TYPE", + Self::UnclassifiedError => "UNCLASSIFIED_ERROR", }; serializer.serialize_str(variant) } @@ -23360,6 +24613,9 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "NOT_FOUND", "NOT_ALLOWED", "LIMIT_EXCEEDED", + "QUEUED", + "UNSUPPORTED_TYPE", + "UNCLASSIFIED_ERROR", ]; struct GeneratedVisitor; @@ -23404,6 +24660,9 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "NOT_FOUND" => Ok(request_response::Reason::NotFound), "NOT_ALLOWED" => Ok(request_response::Reason::NotAllowed), "LIMIT_EXCEEDED" => Ok(request_response::Reason::LimitExceeded), + "QUEUED" => Ok(request_response::Reason::Queued), + "UNSUPPORTED_TYPE" => Ok(request_response::Reason::UnsupportedType), + "UNCLASSIFIED_ERROR" => Ok(request_response::Reason::UnclassifiedError), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -26056,6 +27315,12 @@ impl serde::Serialize for SipCallInfo { if !self.media_encryption.is_empty() { len += 1; } + if !self.pcap_file_link.is_empty() { + len += 1; + } + if !self.call_context.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.SIPCallInfo", len)?; if !self.call_id.is_empty() { struct_ser.serialize_field("callId", &self.call_id)?; @@ -26151,6 +27416,12 @@ impl serde::Serialize for SipCallInfo { if !self.media_encryption.is_empty() { struct_ser.serialize_field("mediaEncryption", &self.media_encryption)?; } + if !self.pcap_file_link.is_empty() { + struct_ser.serialize_field("pcapFileLink", &self.pcap_file_link)?; + } + if !self.call_context.is_empty() { + struct_ser.serialize_field("callContext", &self.call_context)?; + } struct_ser.end() } } @@ -26207,6 +27478,10 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { "audioCodec", "media_encryption", "mediaEncryption", + "pcap_file_link", + "pcapFileLink", + "call_context", + "callContext", ]; #[allow(clippy::enum_variant_names)] @@ -26235,6 +27510,8 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { CallStatusCode, AudioCodec, MediaEncryption, + PcapFileLink, + CallContext, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -26281,6 +27558,8 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { "callStatusCode" | "call_status_code" => Ok(GeneratedField::CallStatusCode), "audioCodec" | "audio_codec" => Ok(GeneratedField::AudioCodec), "mediaEncryption" | "media_encryption" => Ok(GeneratedField::MediaEncryption), + "pcapFileLink" | "pcap_file_link" => Ok(GeneratedField::PcapFileLink), + "callContext" | "call_context" => Ok(GeneratedField::CallContext), _ => Ok(GeneratedField::__SkipField__), } } @@ -26324,6 +27603,8 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { let mut call_status_code__ = None; let mut audio_codec__ = None; let mut media_encryption__ = None; + let mut pcap_file_link__ = None; + let mut call_context__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::CallId => { @@ -26484,6 +27765,18 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { } media_encryption__ = Some(map_.next_value()?); } + GeneratedField::PcapFileLink => { + if pcap_file_link__.is_some() { + return Err(serde::de::Error::duplicate_field("pcapFileLink")); + } + pcap_file_link__ = Some(map_.next_value()?); + } + GeneratedField::CallContext => { + if call_context__.is_some() { + return Err(serde::de::Error::duplicate_field("callContext")); + } + call_context__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -26514,6 +27807,8 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { call_status_code: call_status_code__, audio_codec: audio_codec__.unwrap_or_default(), media_encryption: media_encryption__.unwrap_or_default(), + pcap_file_link: pcap_file_link__.unwrap_or_default(), + call_context: call_context__.unwrap_or_default(), }) } } @@ -32167,6 +33462,12 @@ impl serde::Serialize for SignalRequest { signal_request::Message::UpdateVideoTrack(v) => { struct_ser.serialize_field("updateVideoTrack", v)?; } + signal_request::Message::AddDataTrack(v) => { + struct_ser.serialize_field("addDataTrack", v)?; + } + signal_request::Message::UpdateDataSubscription(v) => { + struct_ser.serialize_field("updateDataSubscription", v)?; + } } } struct_ser.end() @@ -32205,6 +33506,10 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "updateAudioTrack", "update_video_track", "updateVideoTrack", + "add_data_track", + "addDataTrack", + "update_data_subscription", + "updateDataSubscription", ]; #[allow(clippy::enum_variant_names)] @@ -32226,6 +33531,8 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { PingReq, UpdateAudioTrack, UpdateVideoTrack, + AddDataTrack, + UpdateDataSubscription, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -32265,6 +33572,8 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "pingReq" | "ping_req" => Ok(GeneratedField::PingReq), "updateAudioTrack" | "update_audio_track" => Ok(GeneratedField::UpdateAudioTrack), "updateVideoTrack" | "update_video_track" => Ok(GeneratedField::UpdateVideoTrack), + "addDataTrack" | "add_data_track" => Ok(GeneratedField::AddDataTrack), + "updateDataSubscription" | "update_data_subscription" => Ok(GeneratedField::UpdateDataSubscription), _ => Ok(GeneratedField::__SkipField__), } } @@ -32403,6 +33712,20 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { return Err(serde::de::Error::duplicate_field("updateVideoTrack")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UpdateVideoTrack) +; + } + GeneratedField::AddDataTrack => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("addDataTrack")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::AddDataTrack) +; + } + GeneratedField::UpdateDataSubscription => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("updateDataSubscription")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UpdateDataSubscription) ; } GeneratedField::__SkipField__ => { @@ -32506,6 +33829,12 @@ impl serde::Serialize for SignalResponse { signal_response::Message::MediaSectionsRequirement(v) => { struct_ser.serialize_field("mediaSectionsRequirement", v)?; } + signal_response::Message::SubscribedAudioCodecUpdate(v) => { + struct_ser.serialize_field("subscribedAudioCodecUpdate", v)?; + } + signal_response::Message::DataTrackPublished(v) => { + struct_ser.serialize_field("dataTrackPublished", v)?; + } } } struct_ser.end() @@ -32557,6 +33886,10 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "roomMoved", "media_sections_requirement", "mediaSectionsRequirement", + "subscribed_audio_codec_update", + "subscribedAudioCodecUpdate", + "data_track_published", + "dataTrackPublished", ]; #[allow(clippy::enum_variant_names)] @@ -32585,6 +33918,8 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { TrackSubscribed, RoomMoved, MediaSectionsRequirement, + SubscribedAudioCodecUpdate, + DataTrackPublished, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -32631,6 +33966,8 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "trackSubscribed" | "track_subscribed" => Ok(GeneratedField::TrackSubscribed), "roomMoved" | "room_moved" => Ok(GeneratedField::RoomMoved), "mediaSectionsRequirement" | "media_sections_requirement" => Ok(GeneratedField::MediaSectionsRequirement), + "subscribedAudioCodecUpdate" | "subscribed_audio_codec_update" => Ok(GeneratedField::SubscribedAudioCodecUpdate), + "dataTrackPublished" | "data_track_published" => Ok(GeneratedField::DataTrackPublished), _ => Ok(GeneratedField::__SkipField__), } } @@ -32817,6 +34154,20 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { return Err(serde::de::Error::duplicate_field("mediaSectionsRequirement")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::MediaSectionsRequirement) +; + } + GeneratedField::SubscribedAudioCodecUpdate => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("subscribedAudioCodecUpdate")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::SubscribedAudioCodecUpdate) +; + } + GeneratedField::DataTrackPublished => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("dataTrackPublished")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::DataTrackPublished) ; } GeneratedField::__SkipField__ => { @@ -34708,9 +36059,121 @@ impl<'de> serde::Deserialize<'de> for StreamStateInfo { E: serde::de::Error, { match value { - "participantSid" | "participant_sid" => Ok(GeneratedField::ParticipantSid), - "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), - "state" => Ok(GeneratedField::State), + "participantSid" | "participant_sid" => Ok(GeneratedField::ParticipantSid), + "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), + "state" => Ok(GeneratedField::State), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = StreamStateInfo; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.StreamStateInfo") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut participant_sid__ = None; + let mut track_sid__ = None; + let mut state__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::ParticipantSid => { + if participant_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("participantSid")); + } + participant_sid__ = Some(map_.next_value()?); + } + GeneratedField::TrackSid => { + if track_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSid")); + } + track_sid__ = Some(map_.next_value()?); + } + GeneratedField::State => { + if state__.is_some() { + return Err(serde::de::Error::duplicate_field("state")); + } + state__ = Some(map_.next_value::()? as i32); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(StreamStateInfo { + participant_sid: participant_sid__.unwrap_or_default(), + track_sid: track_sid__.unwrap_or_default(), + state: state__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.StreamStateInfo", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for StreamStateUpdate { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.stream_states.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.StreamStateUpdate", len)?; + if !self.stream_states.is_empty() { + struct_ser.serialize_field("streamStates", &self.stream_states)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for StreamStateUpdate { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "stream_states", + "streamStates", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + StreamStates, + __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 { + "streamStates" | "stream_states" => Ok(GeneratedField::StreamStates), _ => Ok(GeneratedField::__SkipField__), } } @@ -34720,55 +36183,151 @@ impl<'de> serde::Deserialize<'de> for StreamStateInfo { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = StreamStateInfo; + type Value = StreamStateUpdate; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.StreamStateInfo") + formatter.write_str("struct livekit.StreamStateUpdate") } - 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 participant_sid__ = None; - let mut track_sid__ = None; - let mut state__ = None; + let mut stream_states__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::ParticipantSid => { - if participant_sid__.is_some() { - return Err(serde::de::Error::duplicate_field("participantSid")); + GeneratedField::StreamStates => { + if stream_states__.is_some() { + return Err(serde::de::Error::duplicate_field("streamStates")); } - participant_sid__ = Some(map_.next_value()?); + stream_states__ = Some(map_.next_value()?); } - GeneratedField::TrackSid => { - if track_sid__.is_some() { - return Err(serde::de::Error::duplicate_field("trackSid")); + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(StreamStateUpdate { + stream_states: stream_states__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.StreamStateUpdate", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for SubscribedAudioCodec { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.codec.is_empty() { + len += 1; + } + if self.enabled { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.SubscribedAudioCodec", len)?; + if !self.codec.is_empty() { + struct_ser.serialize_field("codec", &self.codec)?; + } + if self.enabled { + struct_ser.serialize_field("enabled", &self.enabled)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for SubscribedAudioCodec { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "codec", + "enabled", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Codec, + Enabled, + __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 { + "codec" => Ok(GeneratedField::Codec), + "enabled" => Ok(GeneratedField::Enabled), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = SubscribedAudioCodec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.SubscribedAudioCodec") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut codec__ = None; + let mut enabled__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Codec => { + if codec__.is_some() { + return Err(serde::de::Error::duplicate_field("codec")); } - track_sid__ = Some(map_.next_value()?); + codec__ = Some(map_.next_value()?); } - GeneratedField::State => { - if state__.is_some() { - return Err(serde::de::Error::duplicate_field("state")); + GeneratedField::Enabled => { + if enabled__.is_some() { + return Err(serde::de::Error::duplicate_field("enabled")); } - state__ = Some(map_.next_value::()? as i32); + enabled__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(StreamStateInfo { - participant_sid: participant_sid__.unwrap_or_default(), - track_sid: track_sid__.unwrap_or_default(), - state: state__.unwrap_or_default(), + Ok(SubscribedAudioCodec { + codec: codec__.unwrap_or_default(), + enabled: enabled__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.StreamStateInfo", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.SubscribedAudioCodec", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for StreamStateUpdate { +impl serde::Serialize for SubscribedAudioCodecUpdate { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -34776,30 +36335,39 @@ impl serde::Serialize for StreamStateUpdate { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.stream_states.is_empty() { + if !self.track_sid.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.StreamStateUpdate", len)?; - if !self.stream_states.is_empty() { - struct_ser.serialize_field("streamStates", &self.stream_states)?; + if !self.subscribed_audio_codecs.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.SubscribedAudioCodecUpdate", len)?; + if !self.track_sid.is_empty() { + struct_ser.serialize_field("trackSid", &self.track_sid)?; + } + if !self.subscribed_audio_codecs.is_empty() { + struct_ser.serialize_field("subscribedAudioCodecs", &self.subscribed_audio_codecs)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for StreamStateUpdate { +impl<'de> serde::Deserialize<'de> for SubscribedAudioCodecUpdate { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "stream_states", - "streamStates", + "track_sid", + "trackSid", + "subscribed_audio_codecs", + "subscribedAudioCodecs", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - StreamStates, + TrackSid, + SubscribedAudioCodecs, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -34822,7 +36390,8 @@ impl<'de> serde::Deserialize<'de> for StreamStateUpdate { E: serde::de::Error, { match value { - "streamStates" | "stream_states" => Ok(GeneratedField::StreamStates), + "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), + "subscribedAudioCodecs" | "subscribed_audio_codecs" => Ok(GeneratedField::SubscribedAudioCodecs), _ => Ok(GeneratedField::__SkipField__), } } @@ -34832,36 +36401,44 @@ impl<'de> serde::Deserialize<'de> for StreamStateUpdate { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = StreamStateUpdate; + type Value = SubscribedAudioCodecUpdate; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.StreamStateUpdate") + formatter.write_str("struct livekit.SubscribedAudioCodecUpdate") } - 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 stream_states__ = None; + let mut track_sid__ = None; + let mut subscribed_audio_codecs__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::StreamStates => { - if stream_states__.is_some() { - return Err(serde::de::Error::duplicate_field("streamStates")); + GeneratedField::TrackSid => { + if track_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSid")); } - stream_states__ = Some(map_.next_value()?); + track_sid__ = Some(map_.next_value()?); + } + GeneratedField::SubscribedAudioCodecs => { + if subscribed_audio_codecs__.is_some() { + return Err(serde::de::Error::duplicate_field("subscribedAudioCodecs")); + } + subscribed_audio_codecs__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(StreamStateUpdate { - stream_states: stream_states__.unwrap_or_default(), + Ok(SubscribedAudioCodecUpdate { + track_sid: track_sid__.unwrap_or_default(), + subscribed_audio_codecs: subscribed_audio_codecs__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.StreamStateUpdate", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.SubscribedAudioCodecUpdate", FIELDS, GeneratedVisitor) } } impl serde::Serialize for SubscribedCodec { @@ -38401,6 +39978,136 @@ impl<'de> serde::Deserialize<'de> for TrickleRequest { deserializer.deserialize_struct("livekit.TrickleRequest", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for UpdateDataSubscription { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.track_sids.is_empty() { + len += 1; + } + if self.subscribe { + len += 1; + } + if !self.options.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.UpdateDataSubscription", len)?; + if !self.track_sids.is_empty() { + struct_ser.serialize_field("trackSids", &self.track_sids)?; + } + if self.subscribe { + struct_ser.serialize_field("subscribe", &self.subscribe)?; + } + if !self.options.is_empty() { + struct_ser.serialize_field("options", &self.options)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for UpdateDataSubscription { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "track_sids", + "trackSids", + "subscribe", + "options", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + TrackSids, + Subscribe, + Options, + __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 { + "trackSids" | "track_sids" => Ok(GeneratedField::TrackSids), + "subscribe" => Ok(GeneratedField::Subscribe), + "options" => Ok(GeneratedField::Options), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = UpdateDataSubscription; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.UpdateDataSubscription") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut track_sids__ = None; + let mut subscribe__ = None; + let mut options__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::TrackSids => { + if track_sids__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSids")); + } + track_sids__ = Some(map_.next_value()?); + } + GeneratedField::Subscribe => { + if subscribe__.is_some() { + return Err(serde::de::Error::duplicate_field("subscribe")); + } + subscribe__ = Some(map_.next_value()?); + } + GeneratedField::Options => { + if options__.is_some() { + return Err(serde::de::Error::duplicate_field("options")); + } + options__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(UpdateDataSubscription { + track_sids: track_sids__.unwrap_or_default(), + subscribe: subscribe__.unwrap_or_default(), + options: options__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.UpdateDataSubscription", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for UpdateIngressRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -41842,6 +43549,7 @@ impl serde::Serialize for video_layer::Mode { Self::Unused => "MODE_UNUSED", Self::OneSpatialLayerPerStream => "ONE_SPATIAL_LAYER_PER_STREAM", Self::MultipleSpatialLayersPerStream => "MULTIPLE_SPATIAL_LAYERS_PER_STREAM", + Self::OneSpatialLayerPerStreamIncompleteRtcpSr => "ONE_SPATIAL_LAYER_PER_STREAM_INCOMPLETE_RTCP_SR", }; serializer.serialize_str(variant) } @@ -41856,6 +43564,7 @@ impl<'de> serde::Deserialize<'de> for video_layer::Mode { "MODE_UNUSED", "ONE_SPATIAL_LAYER_PER_STREAM", "MULTIPLE_SPATIAL_LAYERS_PER_STREAM", + "ONE_SPATIAL_LAYER_PER_STREAM_INCOMPLETE_RTCP_SR", ]; struct GeneratedVisitor; @@ -41899,6 +43608,7 @@ impl<'de> serde::Deserialize<'de> for video_layer::Mode { "MODE_UNUSED" => Ok(video_layer::Mode::Unused), "ONE_SPATIAL_LAYER_PER_STREAM" => Ok(video_layer::Mode::OneSpatialLayerPerStream), "MULTIPLE_SPATIAL_LAYERS_PER_STREAM" => Ok(video_layer::Mode::MultipleSpatialLayersPerStream), + "ONE_SPATIAL_LAYER_PER_STREAM_INCOMPLETE_RTCP_SR" => Ok(video_layer::Mode::OneSpatialLayerPerStreamIncompleteRtcpSr), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -42319,6 +44029,9 @@ impl serde::Serialize for WebhookConfig { if !self.signing_key.is_empty() { len += 1; } + if self.filter_params.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.WebhookConfig", len)?; if !self.url.is_empty() { struct_ser.serialize_field("url", &self.url)?; @@ -42326,6 +44039,9 @@ impl serde::Serialize for WebhookConfig { if !self.signing_key.is_empty() { struct_ser.serialize_field("signingKey", &self.signing_key)?; } + if let Some(v) = self.filter_params.as_ref() { + struct_ser.serialize_field("filterParams", v)?; + } struct_ser.end() } } @@ -42339,12 +44055,15 @@ impl<'de> serde::Deserialize<'de> for WebhookConfig { "url", "signing_key", "signingKey", + "filter_params", + "filterParams", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { Url, SigningKey, + FilterParams, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -42369,6 +44088,7 @@ impl<'de> serde::Deserialize<'de> for WebhookConfig { match value { "url" => Ok(GeneratedField::Url), "signingKey" | "signing_key" => Ok(GeneratedField::SigningKey), + "filterParams" | "filter_params" => Ok(GeneratedField::FilterParams), _ => Ok(GeneratedField::__SkipField__), } } @@ -42390,6 +44110,7 @@ impl<'de> serde::Deserialize<'de> for WebhookConfig { { let mut url__ = None; let mut signing_key__ = None; + let mut filter_params__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Url => { @@ -42404,6 +44125,12 @@ impl<'de> serde::Deserialize<'de> for WebhookConfig { } signing_key__ = Some(map_.next_value()?); } + GeneratedField::FilterParams => { + if filter_params__.is_some() { + return Err(serde::de::Error::duplicate_field("filterParams")); + } + filter_params__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -42412,6 +44139,7 @@ impl<'de> serde::Deserialize<'de> for WebhookConfig { Ok(WebhookConfig { url: url__.unwrap_or_default(), signing_key: signing_key__.unwrap_or_default(), + filter_params: filter_params__, }) } } From 4c632d597ac2f5361d6b50e553e7437308413b49 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 17 Oct 2025 12:45:38 +1100 Subject: [PATCH 002/232] Wip --- livekit/src/room/data_track/mime.rs | 93 ++++++++++ livekit/src/room/data_track/mod.rs | 161 ++++++++++++++++++ livekit/src/room/mod.rs | 12 +- .../src/room/participant/local_participant.rs | 14 ++ livekit/src/rtc_engine/mod.rs | 8 + livekit/src/rtc_engine/rtc_session.rs | 31 ++++ livekit/tests/data_track_test.rs | 34 ++++ 7 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 livekit/src/room/data_track/mime.rs create mode 100644 livekit/src/room/data_track/mod.rs create mode 100644 livekit/tests/data_track_test.rs diff --git a/livekit/src/room/data_track/mime.rs b/livekit/src/room/data_track/mime.rs new file mode 100644 index 000000000..c69b575d8 --- /dev/null +++ b/livekit/src/room/data_track/mime.rs @@ -0,0 +1,93 @@ +use std::borrow::Cow; +use std::fmt; +use std::str::FromStr; + +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct Mime(Cow<'static, str>); + +impl Mime { + pub const BINARY: Mime = Mime(Cow::Borrowed("application/octet-stream")); + pub const PLAIN_TEXT: Mime = Mime(Cow::Borrowed("text/plain")); + pub const JSON: Mime = Mime(Cow::Borrowed("application/json")); + pub const YAML: Mime = Mime(Cow::Borrowed("application/yaml")); + pub const PROTOBUF: Mime = Mime(Cow::Borrowed("application/x-protobuf")); +} + +impl fmt::Display for Mime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum MimeError { + #[error("Empty string")] + Empty, + #[error("Unknown top level type")] + UnknownTopLevelType, + #[error("Missing sub-type")] + MissingSubtype, + #[error("Sub-type is empty or contains invalid characters")] + InvalidSubtype, +} + +impl FromStr for Mime { + type Err = MimeError; + fn from_str(s: &str) -> Result { + Self::validate(s)?; + let mime = Mime(Cow::Owned(s.to_string())); + Ok(mime) + } +} + +impl Mime { + + fn validate(s: &str) -> Result<(), MimeError> { + if s.is_empty() { + Err(MimeError::Empty)? + } + let (top_level_type, sub_type) = s.split_once("/").ok_or(MimeError::MissingSubtype)?; + if !Self::TOP_LEVEL_TYPES.contains(&top_level_type) { + Err(MimeError::UnknownTopLevelType)? + } + if !Self::is_valid_subtype(sub_type) { + Err(MimeError::InvalidSubtype)? + } + Ok(()) + } + + /// Defined top level media types: + /// https://www.iana.org/assignments/top-level-media-types/top-level-media-types.xhtml + const TOP_LEVEL_TYPES: &[&str] = &[ + "application", + "text", + "image", + "audio", + "video", + "multipart", + "message", + "model", + "haptics", + "example", + "font", + ]; + + fn is_valid_subtype(sub_type: &str) -> bool { + !sub_type.is_empty() + && sub_type.chars().all(|c| match c { + 'a'..='z' + | 'A'..='Z' + | '0'..='9' + | '!' + | '#' + | '$' + | '&' + | '^' + | '_' + | '.' + | '+' + | '-' => true, + _ => false, + }) + } +} \ No newline at end of file diff --git a/livekit/src/room/data_track/mod.rs b/livekit/src/room/data_track/mod.rs new file mode 100644 index 000000000..dc35e469c --- /dev/null +++ b/livekit/src/room/data_track/mod.rs @@ -0,0 +1,161 @@ +use std::marker::PhantomData; +use std::pin::Pin; + +use crate::{e2ee::EncryptionType, id::TrackSid}; +use futures_util::Stream; +use futures_util::task::{Context, Poll}; +use livekit_protocol::{self as proto, DataTrackPublishedResponse}; + +mod mime; + +// TODO: Remove this +pub use mime::Mime; + +#[derive(Debug, thiserror::Error)] +pub enum DataTrackError {} + +pub type DataTrackResult = Result; + +/// Reserved for future use. +pub mod schema { + /// Raw bytes. + pub struct Bytes; +} + +#[derive(Clone, Debug)] +pub struct DataTrackOptions { + name: String, + disable_e2ee: bool, + mime: Mime, + _schema: PhantomData, +} + +impl DataTrackOptions { + pub(crate) fn into_add_track_request( + self, + use_e2ee: bool + ) -> proto::AddDataTrackRequest { + let encryption = if self.disable_e2ee { + proto::encryption::Type::None + } else { + proto::encryption::Type::Gcm + }; + proto::AddDataTrackRequest { + name: self.name, + mime_type: self.mime.to_string(), + encryption: encryption.into(), + } + } +} + +impl DataTrackOptions { + pub fn with_name(name: &str) -> Self { + Self { + name: name.to_string(), + disable_e2ee: false, + mime: Mime::BINARY, + _schema: PhantomData, + } + } + + pub fn mime(self, mime: Mime) -> Self { + Self { mime, ..self } + } + + pub fn disable_e2ee(self, disabled: bool) -> Self { + Self { disable_e2ee: disabled, ..self } + } +} + +#[derive(Clone, Debug)] +struct DataTrackInfo { + sid: TrackSid, + handle: u16, + name: String, + mime: Mime, + encryption: EncryptionType, +} + +impl DataTrackInfo { + pub fn sid(&self) -> &TrackSid { + &self.sid + } + pub fn name(&self) -> &str { + &self.name + } + pub fn mime(&self) -> &Mime { + &self.mime + } + pub fn uses_e2ee(&self) -> bool { + self.encryption != EncryptionType::None + } +} + +struct DataTrackFrame; + +/// Marker type indicating a [`DataTrack`] belongs to the local participant. +pub struct Local; + +/// Marker type indicating a [`DataTrack`] belongs to a remote participant. +pub struct Remote; + +#[derive(Clone, Debug)] +pub struct DataTrack { + _location: PhantomData, + _schema: PhantomData, + // Need info, way to signal closing by SFU or other +} + +impl DataTrack { + pub fn info(&self) -> DataTrackInfo { + todo!() + } +} + +impl DataTrack { + pub fn publish(&self, frame: impl Into) -> DataTrackResult<()> { + todo!() + } +} + +impl DataTrack { + pub(crate) fn from_info(info: DataTrackInfo) -> Result { + Ok(Self { + _location: PhantomData, + _schema: PhantomData, + + }) + } + + pub fn try_with_schema(self) -> Result, Self> { + todo!() + } +} + +impl DataTrack { + pub fn is_subscribed() -> bool { + // Subscribed as long as there is at least one subscription + todo!() + } + + pub fn subscribe(&self) -> DataTrackResult> { + // TODO: send request, create receiver + todo!() + } + + pub fn subscribe_with_target(&self, target_fps: u32) -> DataTrackResult> { + todo!() + } +} + +pub struct DataTrackSubscription { + _schema: PhantomData +} + +impl Stream for DataTrackSubscription { + type Item = S; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + todo!(); + } +} \ No newline at end of file diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index aebe35a56..9d9f935df 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -45,13 +45,10 @@ pub use self::{ }; pub use crate::rtc_engine::SimulateScenario; use crate::{ - participant::ConnectionQuality, - prelude::*, - registered_audio_filter_plugins, - rtc_engine::{ + data_track::DataTrack, participant::ConnectionQuality, prelude::*, registered_audio_filter_plugins, rtc_engine::{ EngineError, EngineEvent, EngineEvents, EngineOptions, EngineResult, RtcEngine, SessionStats, INITIAL_BUFFERED_AMOUNT_LOW_THRESHOLD, - }, + } }; pub mod data_stream; @@ -61,6 +58,7 @@ pub mod options; pub mod participant; pub mod publication; pub mod track; +pub mod data_track; pub(crate) mod utils; pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -88,6 +86,10 @@ pub enum RoomError { pub enum RoomEvent { ParticipantConnected(RemoteParticipant), ParticipantDisconnected(RemoteParticipant), + // DataTrackPublished { + // track: DataTrack, + // participant: RemoteParticipant + // }, LocalTrackPublished { publication: LocalTrackPublication, track: LocalTrack, diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index a9a2320fd..19ca5dd0e 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -28,6 +28,7 @@ use crate::{ ByteStreamInfo, ByteStreamWriter, StreamByteOptions, StreamResult, StreamTextOptions, TextStreamInfo, TextStreamWriter, }, + data_track::{self, DataTrack, DataTrackOptions, Local}, e2ee::EncryptionType, options::{self, compute_video_encodings, video_layers_from_encodings, TrackPublishOptions}, prelude::*, @@ -227,6 +228,19 @@ impl LocalParticipant { vec } + pub async fn publish_data_track( + &self, + options: DataTrackOptions, + ) -> RoomResult> { + + // TODO: set use_e2ee based on participant info + let req = options.into_add_track_request(false); + let track_info = self.inner.rtc_engine.add_data_track(req).await?; + + todo!() + + } + pub async fn publish_track( &self, track: LocalTrack, diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index cc8da7ce2..2cd5143cc 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -267,6 +267,14 @@ impl RtcEngine { session.simulate_scenario(scenario).await } + pub async fn add_data_track(&self, req: proto::AddDataTrackRequest) -> EngineResult { + let (session, _r_lock) = { + let (handle, _r_lock) = self.inner.wait_reconnection().await?; + (handle.session.clone(), _r_lock) + }; + session.add_data_track(req).await + } + pub async fn add_track(&self, req: proto::AddTrackRequest) -> EngineResult { let (session, _r_lock) = { let (handle, _r_lock) = self.inner.wait_reconnection().await?; diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index d3dcb9056..950662b57 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -115,6 +115,10 @@ pub enum SessionEvent { kind: DataPacketKind, encryption_type: proto::encryption::Type, }, + DataTrackPublishedResponse { + request_id: u32, + result: proto::data_track_published_response::Result + }, ChatMessage { participant_identity: ParticipantIdentity, message: ChatMessage, @@ -519,6 +523,13 @@ impl RtcSession { self.inner.publisher_negotiation_needed() } + pub async fn add_data_track( + &self, + req: proto::AddDataTrackRequest, + ) -> EngineResult { + self.inner.add_data_track(req).await + } + pub async fn add_track(&self, req: proto::AddTrackRequest) -> EngineResult { self.inner.add_track(req).await } @@ -974,6 +985,14 @@ impl SessionInner { let _ = self.emitter.send(SessionEvent::ConnectionQuality { updates: quality.updates }); } + proto::signal_response::Message::DataTrackPublished(publish_res) => { + // TODO: Why is this optional? + let Some(result) = publish_res.result else { return Ok(()) }; + let _ = self.emitter.send(SessionEvent::DataTrackPublishedResponse { + request_id: publish_res.request_id, + result + }); + } proto::signal_response::Message::TrackPublished(publish_res) => { let mut pending_tracks = self.pending_tracks.lock(); if let Some(tx) = pending_tracks.remove(&publish_res.cid) { @@ -1273,6 +1292,18 @@ impl SessionInner { } } + async fn add_data_track( + &self, + req: proto::AddDataTrackRequest, + ) -> EngineResult { + // TODO: verify unique name before sending request + self.signal_client.send(proto::signal_request::Message::AddDataTrack(req)).await; + + // TODO: await response + + todo!() + } + async fn add_track(&self, req: proto::AddTrackRequest) -> EngineResult { let (tx, rx) = oneshot::channel(); let cid = req.cid.clone(); diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs new file mode 100644 index 000000000..438477e22 --- /dev/null +++ b/livekit/tests/data_track_test.rs @@ -0,0 +1,34 @@ +#[cfg(feature = "__lk-e2e-test")] +use { + anyhow::{Ok, Result}, + common::test_rooms, + livekit::data_track::{schema, Mime}, +}; + +mod common; + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_data_track() -> Result<()> { + use livekit::data_track::DataTrackOptions; + + let (room, mut event_rx) = test_rooms(1).await?.pop().unwrap(); + + let options = DataTrackOptions::with_name("led_color") + .mime(Mime::JSON) + .disable_e2ee(false); + + let track = room.local_participant().publish_data_track(options).await?; + for idx in 1..25 { + // track.publish() + } + + while let Some(event) = event_rx.recv().await { + use livekit::RoomEvent; + let RoomEvent::TrackPublished { publication, participant } = event else { continue }; + + } + + + Ok(()) +} From e11b8d02e0a4e66d01b4ed64d1d4726f7b4f1d69 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 18 Oct 2025 11:40:24 +1100 Subject: [PATCH 003/232] Wip --- Cargo.lock | 34 +++- Cargo.toml | 2 + livekit-datatrack/Cargo.toml | 14 ++ livekit-datatrack/README.md | 4 + livekit-datatrack/src/api.rs | 143 ++++++++++++++ livekit-datatrack/src/dtp/mod.rs | 1 + livekit-datatrack/src/lib.rs | 18 ++ livekit-datatrack/src/manager/mod.rs | 19 ++ livekit-datatrack/src/manager/publish.rs | 91 +++++++++ livekit-datatrack/src/manager/subscribe.rs | 50 +++++ .../src}/mime.rs | 14 ++ livekit/Cargo.toml | 1 + livekit/src/room/data_track/mod.rs | 177 ++---------------- 13 files changed, 403 insertions(+), 165 deletions(-) create mode 100644 livekit-datatrack/Cargo.toml create mode 100644 livekit-datatrack/README.md create mode 100644 livekit-datatrack/src/api.rs create mode 100644 livekit-datatrack/src/dtp/mod.rs create mode 100644 livekit-datatrack/src/lib.rs create mode 100644 livekit-datatrack/src/manager/mod.rs create mode 100644 livekit-datatrack/src/manager/publish.rs create mode 100644 livekit-datatrack/src/manager/subscribe.rs rename {livekit/src/room/data_track => livekit-datatrack/src}/mime.rs (80%) diff --git a/Cargo.lock b/Cargo.lock index 940ee82c0..98ea58148 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1283,6 +1283,20 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "dasp_sample" version = "0.11.0" @@ -2979,6 +2993,7 @@ dependencies = [ "libloading", "libwebrtc", "livekit-api", + "livekit-datatrack", "livekit-protocol", "livekit-runtime", "log", @@ -3022,13 +3037,24 @@ dependencies = [ "url", ] +[[package]] +name = "livekit-datatrack" +version = "0.1.0" +dependencies = [ + "dashmap 6.1.0", + "livekit-protocol", + "log", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "livekit-ffi" version = "0.12.39" dependencies = [ "bytes", "console-subscriber", - "dashmap", + "dashmap 5.5.3", "downcast-rs", "env_logger 0.10.2", "from_variants", @@ -6690,14 +6716,14 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.5", + "windows-targets 0.53.4", ] [[package]] name = "windows-sys" -version = "0.61.2" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" dependencies = [ "windows-link", ] diff --git a/Cargo.toml b/Cargo.toml index 03f91301f..e644eeaa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "livekit-protocol", "livekit-ffi", "livekit-runtime", + "livekit-datatrack", "libwebrtc", "soxr-sys", "yuv-sys", @@ -36,6 +37,7 @@ livekit-api = { version = "0.4.9", path = "livekit-api" } livekit-ffi = { version = "0.12.39", path = "livekit-ffi" } livekit-protocol = { version = "0.5.1", path = "livekit-protocol" } livekit-runtime = { version = "0.4.0", path = "livekit-runtime" } +livekit-datatrack = { version = "0.1.0", path = "livekit-datatrack" } soxr-sys = { version = "0.1.1", path = "soxr-sys" } webrtc-sys = { version = "0.3.16", path = "webrtc-sys" } webrtc-sys-build = { version = "0.3.11", path = "webrtc-sys/build" } diff --git a/livekit-datatrack/Cargo.toml b/livekit-datatrack/Cargo.toml new file mode 100644 index 000000000..43e74eea7 --- /dev/null +++ b/livekit-datatrack/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "livekit-datatrack" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/livekit/rust-sdks" +readme = "README.md" + +[dependencies] +dashmap = "6.1.0" +livekit-protocol = { workspace = true } +log = "0.4.28" +thiserror = "2.0.17" +tokio = { version = "1.48.0", default-features = false, features = ["sync"] } diff --git a/livekit-datatrack/README.md b/livekit-datatrack/README.md new file mode 100644 index 000000000..88c1938a2 --- /dev/null +++ b/livekit-datatrack/README.md @@ -0,0 +1,4 @@ +# Livekit Data Track + +This is an internal crate used by the LiveKit Rust SDK. + diff --git a/livekit-datatrack/src/api.rs b/livekit-datatrack/src/api.rs new file mode 100644 index 000000000..297a99d49 --- /dev/null +++ b/livekit-datatrack/src/api.rs @@ -0,0 +1,143 @@ +// Copyright 2025 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 crate::mime::Mime; +use std::marker::PhantomData; +use thiserror::Error; + +/// Options for publishing a data track. +#[derive(Clone, Debug)] +pub struct PublishOptions { + pub(crate) name: String, + pub(crate) disable_e2ee: bool, + pub(crate) mime: Mime, +} + +impl PublishOptions { + pub fn with_name(name: impl Into) -> Self { + Self { name: name.into(), disable_e2ee: false, mime: Mime::BINARY } + } + + pub fn mime(self, mime: Mime) -> Self { + Self { mime, ..self } + } + + pub fn disable_e2ee(self, disabled: bool) -> Self { + Self { disable_e2ee: disabled, ..self } + } +} + +#[derive(Clone, Debug)] +struct DataTrackInfo { + sid: String, // TODO: use shared ID type + handle: u16, + name: String, + mime: Mime, + uses_e2ee: bool +} + +impl DataTrackInfo { + pub fn sid(&self) -> &String { + &self.sid + } + pub fn name(&self) -> &str { + &self.name + } + pub fn mime(&self) -> &Mime { + &self.mime + } + pub fn uses_e2ee(&self) -> bool { + self.uses_e2ee + } +} + +#[derive(Debug, Error)] +pub enum PublishError { + #[error("")] // TODO: descriptions + NameTaken, + #[error("")] + Unauthorized, +} + +/// Marker type indicating a [`DataTrack`] belongs to the local participant. +pub struct Local; + +/// Marker type indicating a [`DataTrack`] belongs to a remote participant. +pub struct Remote; + +#[derive(Clone, Debug)] +pub struct DataTrack { + /// Marker indicating local or remote. + inner: Inner, + _location: PhantomData, + // Need info, way to signal closing by SFU or other + + // Cases: + // Local (publish) -> channel tx + // Remote (subscribe) -> channel rx +} + +/* +#[derive(Clone, Debug)] +pub struct DataTrack { + // Marker indicating local or remote. + _location: PhantomData, + // Need info, way to signal closing by SFU or other +} + +impl DataTrack { + pub fn info(&self) -> DataTrackInfo { + todo!() + } +} + +impl DataTrack { + pub fn publish(&self, frame: impl Into) -> DataTrackResult<()> { + todo!() + } +} + +impl DataTrack { + pub(crate) fn from_info(info: DataTrackInfo) -> Result { + Ok(Self { _location: PhantomData }) + } + + pub fn is_subscribed() -> bool { + // Subscribed as long as there is at least one subscription + todo!() + } + + pub fn subscribe(&self) -> DataTrackResult { + // TODO: send request, create receiver + todo!() + } + + pub fn subscribe_with_target( + &self, + target_fps: u32, + ) -> DataTrackResult { + todo!() + } +} + +pub struct DataTrackSubscription; + +impl Stream for DataTrackSubscription { + type Item = S; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + todo!(); + } +} +*/ \ No newline at end of file diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs new file mode 100644 index 000000000..563c0fa97 --- /dev/null +++ b/livekit-datatrack/src/dtp/mod.rs @@ -0,0 +1 @@ +// TODO: Move DTP implementation here \ No newline at end of file diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs new file mode 100644 index 000000000..829f9ed80 --- /dev/null +++ b/livekit-datatrack/src/lib.rs @@ -0,0 +1,18 @@ +// Copyright 2025 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. + +pub mod api; +mod dtp; +mod mime; +mod manager; \ No newline at end of file diff --git a/livekit-datatrack/src/manager/mod.rs b/livekit-datatrack/src/manager/mod.rs new file mode 100644 index 000000000..6cd39c9c0 --- /dev/null +++ b/livekit-datatrack/src/manager/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2025 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. + +pub mod publish; +pub mod subscribe; + +#[derive(PartialEq, Eq, Hash, Debug)] +pub struct TrackHandle(u16); \ No newline at end of file diff --git a/livekit-datatrack/src/manager/publish.rs b/livekit-datatrack/src/manager/publish.rs new file mode 100644 index 000000000..6f28922aa --- /dev/null +++ b/livekit-datatrack/src/manager/publish.rs @@ -0,0 +1,91 @@ +// Copyright 2025 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 super::TrackHandle; +use crate::api::{DataTrack, Local, PublishError, PublishOptions}; +use dashmap::DashMap; +use livekit_protocol as proto; + +// Question: mechanism for signaling tx/rx. Options: +// 1. +// 2. + +#[derive(Debug)] +pub struct PubManagerOptions { + // Dependencies: + // - E2EE + // - Signaling + // - Tx AddTrackRequest, UnpublishDataTrackRequest + // - Rx DataTrackPublishedResponse, DataTrackUnpublishedResponse, RequestResponse + // - Data track channel + // - Tx +} + +#[derive(Debug)] +pub struct PubManager { + options: PubManagerOptions, + pub_tracks: DashMap, +} + +impl PubManager { + pub fn new(options: PubManagerOptions) -> Self { + Self { options, pub_tracks: DashMap::default() } + } + + // from track published, participant update? + pub fn update_state() { + todo!() + } + + pub async fn publish(options: PublishOptions) -> Result, PublishError> { + let request = options.into_add_track_request(false); // set based on E2EE options + + // TODO: send request, await response + let response = proto::DataTrackPublishedResponse::default(); + + + todo!() + } + + // handle update +} + +#[derive(Debug)] +struct Descriptor { + // minimal info + // stats (fps calculation, total sent, etc.) + // rx + // publish -> dtp encode -> data channel +} + +impl Into for PublishOptions { + fn into(self) -> proto::AddDataTrackRequest { + todo!() + } +} + +impl PublishOptions { + fn into_add_track_request(self, use_e2ee: bool) -> proto::AddDataTrackRequest { + let encryption = if self.disable_e2ee || !use_e2ee { + proto::encryption::Type::None + } else { + proto::encryption::Type::Gcm + }; + proto::AddDataTrackRequest { + name: self.name, + mime_type: self.mime.to_string(), + encryption: encryption.into(), + } + } +} diff --git a/livekit-datatrack/src/manager/subscribe.rs b/livekit-datatrack/src/manager/subscribe.rs new file mode 100644 index 000000000..23370277c --- /dev/null +++ b/livekit-datatrack/src/manager/subscribe.rs @@ -0,0 +1,50 @@ +// Copyright 2025 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 dashmap::DashMap; +use super::TrackHandle; + +#[derive(Debug)] +pub struct SubManagerOptions { + // Dependencies + // - E2EE + // - Signaling + // - Tx UpdateDataSubscription + // - Rx DataTrackPublishedResponse, DataTrackUnpublishedResponse, + // - Data track channel + // - Rx +} + +#[derive(Debug)] +pub struct SubManager { + options: SubManagerOptions, + sub_tracks: DashMap +} + +impl SubManager { + pub fn new(options: SubManagerOptions) -> Self { + Self { options, sub_tracks: DashMap::default() } + } +} + +// handles: track published/unpublished +// maintains state +// creates DataTrack when track published +// send signal to existing ones when track unpublished + +#[derive(Debug)] +pub struct Descriptor { + // tx + // data channel -> dtp decode -> frame -> tx +} \ No newline at end of file diff --git a/livekit/src/room/data_track/mime.rs b/livekit-datatrack/src/mime.rs similarity index 80% rename from livekit/src/room/data_track/mime.rs rename to livekit-datatrack/src/mime.rs index c69b575d8..615418f18 100644 --- a/livekit/src/room/data_track/mime.rs +++ b/livekit-datatrack/src/mime.rs @@ -1,3 +1,17 @@ +// Copyright 2025 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 std::borrow::Cow; use std::fmt; use std::str::FromStr; diff --git a/livekit/Cargo.toml b/livekit/Cargo.toml index b65bf3990..b0d246fce 100644 --- a/livekit/Cargo.toml +++ b/livekit/Cargo.toml @@ -28,6 +28,7 @@ __lk-e2e-test = [] # end-to-end testing with a LiveKit server [dependencies] livekit-runtime = { workspace = true } livekit-api = { workspace = true } +livekit-datatrack = { workspace = true } libwebrtc = { workspace = true } livekit-protocol = { workspace = true } prost = "0.12" diff --git a/livekit/src/room/data_track/mod.rs b/livekit/src/room/data_track/mod.rs index dc35e469c..ec8941443 100644 --- a/livekit/src/room/data_track/mod.rs +++ b/livekit/src/room/data_track/mod.rs @@ -1,161 +1,16 @@ -use std::marker::PhantomData; -use std::pin::Pin; - -use crate::{e2ee::EncryptionType, id::TrackSid}; -use futures_util::Stream; -use futures_util::task::{Context, Poll}; -use livekit_protocol::{self as proto, DataTrackPublishedResponse}; - -mod mime; - -// TODO: Remove this -pub use mime::Mime; - -#[derive(Debug, thiserror::Error)] -pub enum DataTrackError {} - -pub type DataTrackResult = Result; - -/// Reserved for future use. -pub mod schema { - /// Raw bytes. - pub struct Bytes; -} - -#[derive(Clone, Debug)] -pub struct DataTrackOptions { - name: String, - disable_e2ee: bool, - mime: Mime, - _schema: PhantomData, -} - -impl DataTrackOptions { - pub(crate) fn into_add_track_request( - self, - use_e2ee: bool - ) -> proto::AddDataTrackRequest { - let encryption = if self.disable_e2ee { - proto::encryption::Type::None - } else { - proto::encryption::Type::Gcm - }; - proto::AddDataTrackRequest { - name: self.name, - mime_type: self.mime.to_string(), - encryption: encryption.into(), - } - } -} - -impl DataTrackOptions { - pub fn with_name(name: &str) -> Self { - Self { - name: name.to_string(), - disable_e2ee: false, - mime: Mime::BINARY, - _schema: PhantomData, - } - } - - pub fn mime(self, mime: Mime) -> Self { - Self { mime, ..self } - } - - pub fn disable_e2ee(self, disabled: bool) -> Self { - Self { disable_e2ee: disabled, ..self } - } -} - -#[derive(Clone, Debug)] -struct DataTrackInfo { - sid: TrackSid, - handle: u16, - name: String, - mime: Mime, - encryption: EncryptionType, -} - -impl DataTrackInfo { - pub fn sid(&self) -> &TrackSid { - &self.sid - } - pub fn name(&self) -> &str { - &self.name - } - pub fn mime(&self) -> &Mime { - &self.mime - } - pub fn uses_e2ee(&self) -> bool { - self.encryption != EncryptionType::None - } -} - -struct DataTrackFrame; - -/// Marker type indicating a [`DataTrack`] belongs to the local participant. -pub struct Local; - -/// Marker type indicating a [`DataTrack`] belongs to a remote participant. -pub struct Remote; - -#[derive(Clone, Debug)] -pub struct DataTrack { - _location: PhantomData, - _schema: PhantomData, - // Need info, way to signal closing by SFU or other -} - -impl DataTrack { - pub fn info(&self) -> DataTrackInfo { - todo!() - } -} - -impl DataTrack { - pub fn publish(&self, frame: impl Into) -> DataTrackResult<()> { - todo!() - } -} - -impl DataTrack { - pub(crate) fn from_info(info: DataTrackInfo) -> Result { - Ok(Self { - _location: PhantomData, - _schema: PhantomData, - - }) - } - - pub fn try_with_schema(self) -> Result, Self> { - todo!() - } -} - -impl DataTrack { - pub fn is_subscribed() -> bool { - // Subscribed as long as there is at least one subscription - todo!() - } - - pub fn subscribe(&self) -> DataTrackResult> { - // TODO: send request, create receiver - todo!() - } - - pub fn subscribe_with_target(&self, target_fps: u32) -> DataTrackResult> { - todo!() - } -} - -pub struct DataTrackSubscription { - _schema: PhantomData -} - -impl Stream for DataTrackSubscription { - type Item = S; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { - todo!(); - } -} \ No newline at end of file +// Copyright 2025 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. + +// Re-export APIs from the data track crate. +pub use livekit_datatrack::api::*; \ No newline at end of file From b8af013d4b78d16916be83467dbe6b951565cd6a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sun, 19 Oct 2025 17:18:14 +1100 Subject: [PATCH 004/232] Migrate data track packet implementation --- livekit-datatrack/src/dtp/builder.rs | 222 ++++++++++++++++++++++++++ livekit-datatrack/src/dtp/common.rs | 69 ++++++++ livekit-datatrack/src/dtp/mod.rs | 22 ++- livekit-datatrack/src/dtp/parse.rs | 229 +++++++++++++++++++++++++++ 4 files changed, 541 insertions(+), 1 deletion(-) create mode 100644 livekit-datatrack/src/dtp/builder.rs create mode 100644 livekit-datatrack/src/dtp/common.rs create mode 100644 livekit-datatrack/src/dtp/parse.rs diff --git a/livekit-datatrack/src/dtp/builder.rs b/livekit-datatrack/src/dtp/builder.rs new file mode 100644 index 000000000..a258547e7 --- /dev/null +++ b/livekit-datatrack/src/dtp/builder.rs @@ -0,0 +1,222 @@ +// Copyright 2025 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 super::common::{consts::*, Encryption, Iv}; +use std::io::{Cursor, Write}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum BuildError { + #[error("Buffer not long enough to contain packet")] + TooShort, +} + +#[derive(Clone)] +pub struct DtpBuilder<'a> { + is_final: bool, + encryption: Option>, + payload: Option<&'a [u8]>, + track_handle: u16, + sequence: u16, + timestamp: Option, + user_timestamp: Option, +} + +impl<'a> DtpBuilder<'a> { + pub fn new() -> Self { + DtpBuilder { + is_final: true, + encryption: None, + payload: None, + track_handle: 0, + sequence: 0, + timestamp: None, + user_timestamp: None, + } + } + + pub fn is_final(mut self, is_final: bool) -> Self { + self.is_final = is_final; + self + } + + pub fn track_handle(mut self, track_handle: u16) -> Self { + self.track_handle = track_handle; + self + } + + pub fn sequence(mut self, sequence: u16) -> Self { + self.sequence = sequence; + self + } + + pub fn encryption(mut self, key_index: u8, iv: &'a Iv) -> Self { + let encryption = Encryption { key_index, iv }; + self.encryption = encryption.into(); + self + } + + pub fn timestamp(mut self, timestamp: u32) -> Self { + self.timestamp = timestamp.into(); + self + } + + pub fn user_timestamp(mut self, user_timestamp: u64) -> Self { + self.user_timestamp = user_timestamp.into(); + self + } + + pub fn payload(mut self, payload: &'a [u8]) -> Self { + self.payload = Some(payload); + self + } + + fn extension_len(&self) -> usize { + let mut len = 0; + if self.timestamp.is_some() { + len += TIMESTAMP_EXT_LEN; + } + if self.user_timestamp.is_some() { + len += USER_TIMESTAMP_EXT_LEN; + } + if self.encryption.is_some() { + len += E2EE_EXT_LEN; + } + len + } + + fn built_len(&self) -> usize { + let mut len = BASE_HEADER_LEN + self.extension_len(); + if let Some(payload) = self.payload { + len += payload.len(); + } + len + } + + fn build_into_unchecked(&self, target: &mut [u8]) -> usize { + target[0] = SUPPORTED_VERSION << VERSION_SHIFT; + if self.is_final { + target[0] |= 1 << FINAL_FLAG_SHIFT; + } + if self.encryption.is_some() { + target[0] |= 1 << E2EE_FLAG_SHIFT; + } + if self.timestamp.is_some() { + target[0] |= 1 << TS_FLAG_SHIFT; + } + if self.user_timestamp.is_some() { + target[0] |= 1 << UTS_FLAG_SHIFT; + } + target[1] = 0; // reserved + target[2] = 0; // reserved + target[EXT_WORDS_OFFSET] = (self.extension_len() / 4) as u8; + target[TRACK_HANDLE_OFFSET..(TRACK_HANDLE_OFFSET + TRACK_HANDLE_LEN)] + .copy_from_slice(&self.track_handle.to_be_bytes()); + target[SEQUENCE_OFFSET..(SEQUENCE_OFFSET + SEQUENCE_LEN)] + .copy_from_slice(&self.sequence.to_be_bytes()); + + let mut cursor = Cursor::new(target); + cursor.set_position(EXT_START_OFFSET as u64); + if let Some(timestamp) = self.timestamp { + cursor.write(×tamp.to_be_bytes()).unwrap(); + } + if let Some(user_timestamp) = self.user_timestamp { + cursor.write(&user_timestamp.to_be_bytes()).unwrap(); + } + if let Some(encryption) = &self.encryption { + cursor.write(encryption.iv).unwrap(); + cursor.write(&[0x00; 3]).unwrap(); // reserved + cursor.write(&[encryption.key_index]).unwrap(); + } + if let Some(payload) = self.payload { + cursor.write(payload).unwrap(); + } + cursor.position() as usize + } + + pub fn build_into(&self, target: &mut [u8]) -> Result { + let built_len = self.built_len(); + if target.len() < built_len { + Err(BuildError::TooShort)? + } + let bytes_written = self.build_into_unchecked(target); + assert_eq!(bytes_written, built_len); + Ok(bytes_written) + } + + pub fn build(&self) -> Vec { + let built_len = self.built_len(); + let mut target = vec![0; built_len]; + let bytes_written = self.build_into_unchecked(&mut target); + assert_eq!(bytes_written, built_len); + target + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Read; + + #[test] + pub fn test_build_into_too_short() { + let mut buffer = [0; 4]; + let result = DtpBuilder::new().build_into(&mut buffer); + assert!(matches!(result, Err(BuildError::TooShort))); + } + + #[test] + pub fn test_build() { + let packet = DtpBuilder::new() + .is_final(true) + .track_handle(1195) + .sequence(1234) + .encryption(128, &[0xFA; 12]) + .timestamp(16_910_400) + .user_timestamp(72_058_693_566_333_184) + .payload(&[0xFA, 0xAF]) + .build(); + + assert_eq!(packet.len(), 38, "Unexpected length"); + + let mut cursor = Cursor::new(packet.as_slice()); + assert_eq!(cursor.read_n(), [0x1E]); + assert_eq!(cursor.read_n(), [0x00; 2], "Reserved bytes should be zero"); + assert_eq!(cursor.read_n(), [0x07], "Extension word count incorrect"); + assert_eq!(cursor.read_n(), [0x04, 0xAB], "Track handle incorrect"); + assert_eq!(cursor.read_n(), [0x04, 0xD2], "Sequence incorrect"); + + assert_eq!(cursor.read_n(), [0x01, 0x02, 0x08, 0x40], "Timestamp incorrect"); + assert_eq!( + cursor.read_n(), + [0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00], + "User timestamp incorrect" + ); + assert_eq!(cursor.read_n(), [0xFA; 12], "IV incorrect"); + assert_eq!(cursor.read_n(), [0x00; 3], "Expected reserved bytes after IV"); + assert_eq!(cursor.read_n(), [0x80], "Expected reserved bytes after IV"); + assert_eq!(cursor.read_n(), [0xFA, 0xAF], "Payload incorrect"); + } + + trait ReadNExt { + fn read_n(&mut self) -> [u8; N]; + } + impl ReadNExt for Cursor<&[u8]> { + fn read_n(&mut self) -> [u8; N] { + let mut buf = [0u8; N]; + self.read_exact(&mut buf).unwrap(); + buf + } + } +} diff --git a/livekit-datatrack/src/dtp/common.rs b/livekit-datatrack/src/dtp/common.rs new file mode 100644 index 000000000..13a599e71 --- /dev/null +++ b/livekit-datatrack/src/dtp/common.rs @@ -0,0 +1,69 @@ +// Copyright 2025 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. + +#[derive(Clone)] +pub struct Encryption<'a> { + pub(crate) key_index: u8, + pub(crate) iv: &'a Iv, +} + +impl<'a> Encryption<'a> { + pub fn key_index(&self) -> u8 { + self.key_index + } + pub fn iv(&self) -> &Iv { + self.iv + } +} + +/// AES initialization vector. +pub type Iv = [u8; consts::E2EE_EXT_IV_LEN]; + +pub(crate) mod consts { + pub const SUPPORTED_VERSION: u8 = 0; + pub const BASE_HEADER_LEN: usize = 8; + + // Bitfield shifts and masks for header flags + pub const VERSION_SHIFT: u8 = 5; + pub const VERSION_MASK: u8 = 0xE0; + pub const FINAL_FLAG_SHIFT: u8 = 4; + pub const FINAL_FLAG_MASK: u8 = 0x10; + pub const E2EE_FLAG_SHIFT: u8 = 3; + pub const E2EE_FLAG_MASK: u8 = 0x08; + pub const TS_FLAG_SHIFT: u8 = 2; + pub const TS_FLAG_MASK: u8 = 0x04; + pub const UTS_FLAG_SHIFT: u8 = 1; + pub const UTS_FLAG_MASK: u8 = 0x02; + + // Header field offsets and lengths + pub const EXT_WORDS_OFFSET: usize = 3; + pub const EXT_WORDS_LEN: usize = 1; + pub const TRACK_HANDLE_OFFSET: usize = 4; + pub const TRACK_HANDLE_LEN: usize = 2; + pub const SEQUENCE_OFFSET: usize = 6; + pub const SEQUENCE_LEN: usize = 2; + + // Start of extension fields + pub const EXT_START_OFFSET: usize = BASE_HEADER_LEN; + + // Extension lengths + pub const TIMESTAMP_EXT_LEN: usize = 4; + pub const USER_TIMESTAMP_EXT_LEN: usize = 8; + pub const E2EE_EXT_LEN: usize = E2EE_EXT_IV_LEN + 3 + E2EE_EXT_KEY_INDEX_LEN; + + // E2EE offsets and lengths + pub const E2EE_EXT_IV_LEN: usize = 12; + pub const E2EE_EXT_KEY_INDEX_LEN: usize = 1; + pub const E2EE_EXT_KEY_INDEX_OFFSET: usize = 15; +} diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index 563c0fa97..c0b408b24 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -1 +1,21 @@ -// TODO: Move DTP implementation here \ No newline at end of file +// Copyright 2025 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. + +mod builder; +mod common; +mod parse; + +pub use builder::{DtpBuilder, BuildError}; +pub use parse::{Dtp, ParseError}; +pub use common::{Encryption, Iv}; \ No newline at end of file diff --git a/livekit-datatrack/src/dtp/parse.rs b/livekit-datatrack/src/dtp/parse.rs new file mode 100644 index 000000000..c111b250e --- /dev/null +++ b/livekit-datatrack/src/dtp/parse.rs @@ -0,0 +1,229 @@ +// Copyright 2025 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 super::common::{Encryption, consts::*}; +use core::fmt; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ParseError { + #[error("buffer is too short to contain a valid header")] + TooShort, + + #[error("header with length {0} exceeds buffer length")] + HeaderOverrun(usize), +} + +pub struct Dtp<'a> { + buffer: &'a [u8], +} + +/// Extract an integer of type `t` from a byte slice at a given `offset`, +/// interpreting the bytes as big-endian. +macro_rules! extract { + ($t:ty, $bytes:expr, $offset:expr) => {{ + use std::mem::size_of; + let slice: [u8; size_of::<$t>()] = $bytes[$offset..$offset + size_of::<$t>()] + .try_into() + .unwrap(); + <$t>::from_be_bytes(slice) + }}; +} + +impl<'a> Dtp<'a> { + /// Packet version. + pub fn version(&self) -> u8 { + (self.buffer[0] & VERSION_MASK) >> VERSION_SHIFT + } + + /// Whether the packet's final flag is set. + pub fn is_final(&self) -> bool { + ((self.buffer[0] & FINAL_FLAG_MASK) >> FINAL_FLAG_SHIFT) != 0 + } + + /// Whether the packet is encrypted. + pub fn is_encrypted(&self) -> bool { + ((self.buffer[0] & E2EE_FLAG_MASK) >> E2EE_FLAG_SHIFT) != 0 + } + + /// Whether the packet has a timestamp extension. + pub fn has_timestamp(&self) -> bool { + ((self.buffer[0] & TS_FLAG_MASK) >> TS_FLAG_SHIFT) != 0 + } + + /// Whether the packet has a user timestamp extension. + pub fn has_user_timestamp(&self) -> bool { + ((self.buffer[0] & UTS_FLAG_MASK) >> UTS_FLAG_SHIFT) != 0 + } + + /// Total length of all header extensions expressed in number of 32-bit words. + fn extension_words(&self) -> u8 { + self.buffer[EXT_WORDS_OFFSET] + } + + /// Track handle. + pub fn track_handle(&self) -> u16 { + extract!(u16, self.buffer, TRACK_HANDLE_OFFSET) + } + + /// Sequence number. + pub fn sequence(&self) -> u16 { + extract!(u16, self.buffer, SEQUENCE_OFFSET) + } + + /// Timestamp, if the packet has one. + pub fn timestamp(&self) -> Option { + self.has_timestamp() + .then(|| extract!(u32, self.buffer, EXT_START_OFFSET)) + } + + /// User timestamp, if the packet has one. + pub fn user_timestamp(&self) -> Option { + if !self.has_user_timestamp() { + None? + } + let offset = EXT_START_OFFSET + + self + .has_timestamp() + .then_some(TIMESTAMP_EXT_LEN) + .unwrap_or_default(); + extract!(u64, self.buffer, offset).into() + } + + /// Encryption details. + pub fn encryption(&self) -> Option { + if !self.is_encrypted() { + None? + } + let offset = EXT_START_OFFSET + + self + .has_timestamp() + .then_some(TIMESTAMP_EXT_LEN) + .unwrap_or_default() + + self + .has_user_timestamp() + .then_some(USER_TIMESTAMP_EXT_LEN) + .unwrap_or_default(); + let extension = &self.buffer[offset..(offset + E2EE_EXT_LEN)]; + let iv = extension[..E2EE_EXT_IV_LEN].try_into().unwrap(); + let key_index = extension[E2EE_EXT_KEY_INDEX_OFFSET]; + + Encryption { iv, key_index }.into() + } + + /// The payload section of the packet. + pub fn payload(&self) -> &[u8] { + &self.buffer[self.header_len()..] + } + + fn header_len(&self) -> usize { + (4 * self.extension_words() as usize) + BASE_HEADER_LEN + } +} + +impl<'a> TryFrom<&'a [u8]> for Dtp<'a> { + type Error = ParseError; + + fn try_from(buffer: &'a [u8]) -> Result { + if buffer.len() < BASE_HEADER_LEN { + Err(ParseError::TooShort)? + } + let packet = Dtp { buffer }; + if packet.header_len() > buffer.len() { + Err(ParseError::HeaderOverrun(packet.header_len()))? + } + Ok(packet) + } +} + +impl<'a> fmt::Debug for Dtp<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Dtp") + .field("version", &self.version()) + .field("is_final", &self.is_final()) + .field("is_encrypted", &self.is_encrypted()) + .field("track_handle", &self.track_handle()) + .field("sequence", &self.sequence()) + .field("timestamp", &self.timestamp()) + .field("user_timestamp", &self.user_timestamp()) + .field("payload_len", &self.payload().len()) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_short_buffer() { + let buffer = [0xFF; 4]; + let dtp: Result = buffer.as_ref().try_into(); + assert!(matches!(dtp, Err(ParseError::TooShort))); + } + + #[test] + fn test_header_overrun() { + let buffer = [ + 0x00, 0x00, 0x00, 0x01, // 1 extension word, would overrun buffer + 0x00, 0x00, 0x00, 0x00, + ]; + let dtp: Result = buffer.as_ref().try_into(); + assert!(matches!(dtp, Err(ParseError::HeaderOverrun(12)))); + } + + #[test] + fn test_field_accessors() { + let buffer = [ + 0x1E, 0x00, // Version 0 | E2EE | Timestamp | User Timestamp + 0x00, 0x07, // Extension Words + 0x04, 0xAB, // Track Handle + 0x04, 0xD2, // Sequence + 0x01, 0x02, // Timestamp + 0x08, 0x40, // ... + 0x01, 0x00, // User Timestamp + 0x01, 0x00, // ... + 0x01, 0x00, // ... + 0x01, 0x00, // ... + 0xFA, 0xFA, // E2EE + 0xFA, 0xFA, // ... + 0xFA, 0xFA, // ... + 0xFA, 0xFA, // ... + 0xFA, 0xFA, // ... + 0xFA, 0xFA, // ... + 0x00, 0x00, // Padding + 0x00, 0x48, // Key Index + 0xFA, 0xAF, // Payload + ]; + + let dtp: Dtp = buffer.as_ref().try_into().unwrap(); + assert_eq!(dtp.version(), 0); + assert!(dtp.is_final()); + assert!(dtp.is_encrypted()); + assert!(dtp.has_timestamp()); + assert!(dtp.has_user_timestamp()); + assert_eq!(dtp.track_handle(), 1195); + assert_eq!(dtp.sequence(), 1234); + assert_eq!(dtp.extension_words(), 7); + assert_eq!(dtp.timestamp(), Some(16_910_400)); + assert_eq!(dtp.user_timestamp(), Some(72_058_693_566_333_184)); + + let encryption = dtp.encryption().unwrap(); + assert_eq!(encryption.key_index(), 72); + assert_eq!(encryption.iv(), &[0xFA; 12]); + + assert_eq!(dtp.payload(), &[0xFA, 0xAF]); + println!("{:?}", dtp); + } +} From dd09ea7cbecc855aed9e70d086951197c1fdcde2 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sun, 19 Oct 2025 20:30:24 +1100 Subject: [PATCH 005/232] Wip --- Cargo.lock | 2 + livekit-datatrack/Cargo.toml | 2 + livekit-datatrack/src/error.rs | 23 +++++++++++ livekit-datatrack/src/frame.rs | 46 ++++++++++++++++++++++ livekit-datatrack/src/lib.rs | 4 +- livekit-datatrack/src/manager/publish.rs | 18 ++++++++- livekit-datatrack/src/{api.rs => track.rs} | 33 +++------------- 7 files changed, 99 insertions(+), 29 deletions(-) create mode 100644 livekit-datatrack/src/error.rs create mode 100644 livekit-datatrack/src/frame.rs rename livekit-datatrack/src/{api.rs => track.rs} (83%) diff --git a/Cargo.lock b/Cargo.lock index 98ea58148..ea9cfd1dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3041,7 +3041,9 @@ dependencies = [ name = "livekit-datatrack" version = "0.1.0" dependencies = [ + "bytes", "dashmap 6.1.0", + "futures-util", "livekit-protocol", "log", "thiserror 2.0.17", diff --git a/livekit-datatrack/Cargo.toml b/livekit-datatrack/Cargo.toml index 43e74eea7..c8100c43b 100644 --- a/livekit-datatrack/Cargo.toml +++ b/livekit-datatrack/Cargo.toml @@ -12,3 +12,5 @@ livekit-protocol = { workspace = true } log = "0.4.28" thiserror = "2.0.17" tokio = { version = "1.48.0", default-features = false, features = ["sync"] } +futures-util = { version = "0.3", default-features = false, features = ["sink"] } +bytes = "1.10.1" diff --git a/livekit-datatrack/src/error.rs b/livekit-datatrack/src/error.rs new file mode 100644 index 000000000..b4f6f142a --- /dev/null +++ b/livekit-datatrack/src/error.rs @@ -0,0 +1,23 @@ +// Copyright 2025 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 thiserror::Error; + +#[derive(Debug, Error)] +pub enum PublishError { + #[error("")] // TODO: descriptions + NameTaken, + #[error("")] + Unauthorized, +} \ No newline at end of file diff --git a/livekit-datatrack/src/frame.rs b/livekit-datatrack/src/frame.rs new file mode 100644 index 000000000..d956e0f89 --- /dev/null +++ b/livekit-datatrack/src/frame.rs @@ -0,0 +1,46 @@ +// Copyright 2025 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 bytes::Bytes; + +pub struct DataTrackFrame { + payload: Bytes, +} + +impl DataTrackFrame { + pub fn payload(&self) -> &Bytes { + &self.payload + } +} + +#[derive(Default)] +pub struct DataTrackFrameBuilder { + payload: Bytes, + user_timestamp: Option, +} + +impl DataTrackFrameBuilder { + pub fn new(payload: impl Into) -> Self { + Self { payload: payload.into(), ..Default::default() } + } + + pub fn user_timestamp(mut self, user_timestamp: u64) -> Self { + self.user_timestamp = Some(user_timestamp); + self + } + + pub fn build(self) -> DataTrackFrame { + todo!() + } +} diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index 829f9ed80..4715eee1a 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod api; mod dtp; mod mime; +mod frame; +mod track; +mod error; mod manager; \ No newline at end of file diff --git a/livekit-datatrack/src/manager/publish.rs b/livekit-datatrack/src/manager/publish.rs index 6f28922aa..54437a61a 100644 --- a/livekit-datatrack/src/manager/publish.rs +++ b/livekit-datatrack/src/manager/publish.rs @@ -12,15 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::{error::PublishError, track::{DataTrack, Local, PublishOptions}}; + use super::TrackHandle; -use crate::api::{DataTrack, Local, PublishError, PublishOptions}; use dashmap::DashMap; use livekit_protocol as proto; +use tokio::sync::mpsc; + + // Question: mechanism for signaling tx/rx. Options: // 1. // 2. +enum PubMangerRxMessage { + PublishedResponse(proto::DataTrackPublishedResponse), + UnpublishedResponse(()), + RequestResponse(proto::RequestResponse) +} + +enum PubMangerTxMessage { + AddTrack(proto::AddTrackRequest), + UnpublishRequest(()) +} + #[derive(Debug)] pub struct PubManagerOptions { // Dependencies: @@ -30,6 +45,7 @@ pub struct PubManagerOptions { // - Rx DataTrackPublishedResponse, DataTrackUnpublishedResponse, RequestResponse // - Data track channel // - Tx + transport_tx: mpsc::UnboundedReceiver> } #[derive(Debug)] diff --git a/livekit-datatrack/src/api.rs b/livekit-datatrack/src/track.rs similarity index 83% rename from livekit-datatrack/src/api.rs rename to livekit-datatrack/src/track.rs index 297a99d49..aa44e63bb 100644 --- a/livekit-datatrack/src/api.rs +++ b/livekit-datatrack/src/track.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::mime::Mime; -use std::marker::PhantomData; -use thiserror::Error; +use crate::{frame::DataTrackFrame, mime::Mime}; +use futures_util::{task::Context, Stream}; +use std::{marker::PhantomData, pin::Pin, task::Poll}; /// Options for publishing a data track. #[derive(Clone, Debug)] @@ -44,7 +44,7 @@ struct DataTrackInfo { handle: u16, name: String, mime: Mime, - uses_e2ee: bool + uses_e2ee: bool, } impl DataTrackInfo { @@ -62,14 +62,6 @@ impl DataTrackInfo { } } -#[derive(Debug, Error)] -pub enum PublishError { - #[error("")] // TODO: descriptions - NameTaken, - #[error("")] - Unauthorized, -} - /// Marker type indicating a [`DataTrack`] belongs to the local participant. pub struct Local; @@ -79,7 +71,6 @@ pub struct Remote; #[derive(Clone, Debug)] pub struct DataTrack { /// Marker indicating local or remote. - inner: Inner, _location: PhantomData, // Need info, way to signal closing by SFU or other @@ -88,14 +79,6 @@ pub struct DataTrack { // Remote (subscribe) -> channel rx } -/* -#[derive(Clone, Debug)] -pub struct DataTrack { - // Marker indicating local or remote. - _location: PhantomData, - // Need info, way to signal closing by SFU or other -} - impl DataTrack { pub fn info(&self) -> DataTrackInfo { todo!() @@ -123,10 +106,7 @@ impl DataTrack { todo!() } - pub fn subscribe_with_target( - &self, - target_fps: u32, - ) -> DataTrackResult { + pub fn subscribe_with_target(&self, target_fps: u32) -> DataTrackResult { todo!() } } @@ -134,10 +114,9 @@ impl DataTrack { pub struct DataTrackSubscription; impl Stream for DataTrackSubscription { - type Item = S; + type Item = DataTrackFrame; fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { todo!(); } } -*/ \ No newline at end of file From f180a4387b246633f4298bfc2108e766ad0098bc Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:23:05 +1100 Subject: [PATCH 006/232] Upgrade DTP --- livekit-datatrack/src/dtp/builder.rs | 222 ---------------- livekit-datatrack/src/dtp/common.rs | 69 ----- livekit-datatrack/src/dtp/deserialize.rs | 264 +++++++++++++++++++ livekit-datatrack/src/dtp/{mod.rs => lib.rs} | 11 +- livekit-datatrack/src/dtp/packet.rs | 87 ++++++ livekit-datatrack/src/dtp/parse.rs | 229 ---------------- livekit-datatrack/src/dtp/serialize.rs | 201 ++++++++++++++ livekit-datatrack/src/dtp/track_handle.rs | 49 ++++ 8 files changed, 605 insertions(+), 527 deletions(-) delete mode 100644 livekit-datatrack/src/dtp/builder.rs delete mode 100644 livekit-datatrack/src/dtp/common.rs create mode 100644 livekit-datatrack/src/dtp/deserialize.rs rename livekit-datatrack/src/dtp/{mod.rs => lib.rs} (80%) create mode 100644 livekit-datatrack/src/dtp/packet.rs delete mode 100644 livekit-datatrack/src/dtp/parse.rs create mode 100644 livekit-datatrack/src/dtp/serialize.rs create mode 100644 livekit-datatrack/src/dtp/track_handle.rs diff --git a/livekit-datatrack/src/dtp/builder.rs b/livekit-datatrack/src/dtp/builder.rs deleted file mode 100644 index a258547e7..000000000 --- a/livekit-datatrack/src/dtp/builder.rs +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright 2025 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 super::common::{consts::*, Encryption, Iv}; -use std::io::{Cursor, Write}; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum BuildError { - #[error("Buffer not long enough to contain packet")] - TooShort, -} - -#[derive(Clone)] -pub struct DtpBuilder<'a> { - is_final: bool, - encryption: Option>, - payload: Option<&'a [u8]>, - track_handle: u16, - sequence: u16, - timestamp: Option, - user_timestamp: Option, -} - -impl<'a> DtpBuilder<'a> { - pub fn new() -> Self { - DtpBuilder { - is_final: true, - encryption: None, - payload: None, - track_handle: 0, - sequence: 0, - timestamp: None, - user_timestamp: None, - } - } - - pub fn is_final(mut self, is_final: bool) -> Self { - self.is_final = is_final; - self - } - - pub fn track_handle(mut self, track_handle: u16) -> Self { - self.track_handle = track_handle; - self - } - - pub fn sequence(mut self, sequence: u16) -> Self { - self.sequence = sequence; - self - } - - pub fn encryption(mut self, key_index: u8, iv: &'a Iv) -> Self { - let encryption = Encryption { key_index, iv }; - self.encryption = encryption.into(); - self - } - - pub fn timestamp(mut self, timestamp: u32) -> Self { - self.timestamp = timestamp.into(); - self - } - - pub fn user_timestamp(mut self, user_timestamp: u64) -> Self { - self.user_timestamp = user_timestamp.into(); - self - } - - pub fn payload(mut self, payload: &'a [u8]) -> Self { - self.payload = Some(payload); - self - } - - fn extension_len(&self) -> usize { - let mut len = 0; - if self.timestamp.is_some() { - len += TIMESTAMP_EXT_LEN; - } - if self.user_timestamp.is_some() { - len += USER_TIMESTAMP_EXT_LEN; - } - if self.encryption.is_some() { - len += E2EE_EXT_LEN; - } - len - } - - fn built_len(&self) -> usize { - let mut len = BASE_HEADER_LEN + self.extension_len(); - if let Some(payload) = self.payload { - len += payload.len(); - } - len - } - - fn build_into_unchecked(&self, target: &mut [u8]) -> usize { - target[0] = SUPPORTED_VERSION << VERSION_SHIFT; - if self.is_final { - target[0] |= 1 << FINAL_FLAG_SHIFT; - } - if self.encryption.is_some() { - target[0] |= 1 << E2EE_FLAG_SHIFT; - } - if self.timestamp.is_some() { - target[0] |= 1 << TS_FLAG_SHIFT; - } - if self.user_timestamp.is_some() { - target[0] |= 1 << UTS_FLAG_SHIFT; - } - target[1] = 0; // reserved - target[2] = 0; // reserved - target[EXT_WORDS_OFFSET] = (self.extension_len() / 4) as u8; - target[TRACK_HANDLE_OFFSET..(TRACK_HANDLE_OFFSET + TRACK_HANDLE_LEN)] - .copy_from_slice(&self.track_handle.to_be_bytes()); - target[SEQUENCE_OFFSET..(SEQUENCE_OFFSET + SEQUENCE_LEN)] - .copy_from_slice(&self.sequence.to_be_bytes()); - - let mut cursor = Cursor::new(target); - cursor.set_position(EXT_START_OFFSET as u64); - if let Some(timestamp) = self.timestamp { - cursor.write(×tamp.to_be_bytes()).unwrap(); - } - if let Some(user_timestamp) = self.user_timestamp { - cursor.write(&user_timestamp.to_be_bytes()).unwrap(); - } - if let Some(encryption) = &self.encryption { - cursor.write(encryption.iv).unwrap(); - cursor.write(&[0x00; 3]).unwrap(); // reserved - cursor.write(&[encryption.key_index]).unwrap(); - } - if let Some(payload) = self.payload { - cursor.write(payload).unwrap(); - } - cursor.position() as usize - } - - pub fn build_into(&self, target: &mut [u8]) -> Result { - let built_len = self.built_len(); - if target.len() < built_len { - Err(BuildError::TooShort)? - } - let bytes_written = self.build_into_unchecked(target); - assert_eq!(bytes_written, built_len); - Ok(bytes_written) - } - - pub fn build(&self) -> Vec { - let built_len = self.built_len(); - let mut target = vec![0; built_len]; - let bytes_written = self.build_into_unchecked(&mut target); - assert_eq!(bytes_written, built_len); - target - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Read; - - #[test] - pub fn test_build_into_too_short() { - let mut buffer = [0; 4]; - let result = DtpBuilder::new().build_into(&mut buffer); - assert!(matches!(result, Err(BuildError::TooShort))); - } - - #[test] - pub fn test_build() { - let packet = DtpBuilder::new() - .is_final(true) - .track_handle(1195) - .sequence(1234) - .encryption(128, &[0xFA; 12]) - .timestamp(16_910_400) - .user_timestamp(72_058_693_566_333_184) - .payload(&[0xFA, 0xAF]) - .build(); - - assert_eq!(packet.len(), 38, "Unexpected length"); - - let mut cursor = Cursor::new(packet.as_slice()); - assert_eq!(cursor.read_n(), [0x1E]); - assert_eq!(cursor.read_n(), [0x00; 2], "Reserved bytes should be zero"); - assert_eq!(cursor.read_n(), [0x07], "Extension word count incorrect"); - assert_eq!(cursor.read_n(), [0x04, 0xAB], "Track handle incorrect"); - assert_eq!(cursor.read_n(), [0x04, 0xD2], "Sequence incorrect"); - - assert_eq!(cursor.read_n(), [0x01, 0x02, 0x08, 0x40], "Timestamp incorrect"); - assert_eq!( - cursor.read_n(), - [0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00], - "User timestamp incorrect" - ); - assert_eq!(cursor.read_n(), [0xFA; 12], "IV incorrect"); - assert_eq!(cursor.read_n(), [0x00; 3], "Expected reserved bytes after IV"); - assert_eq!(cursor.read_n(), [0x80], "Expected reserved bytes after IV"); - assert_eq!(cursor.read_n(), [0xFA, 0xAF], "Payload incorrect"); - } - - trait ReadNExt { - fn read_n(&mut self) -> [u8; N]; - } - impl ReadNExt for Cursor<&[u8]> { - fn read_n(&mut self) -> [u8; N] { - let mut buf = [0u8; N]; - self.read_exact(&mut buf).unwrap(); - buf - } - } -} diff --git a/livekit-datatrack/src/dtp/common.rs b/livekit-datatrack/src/dtp/common.rs deleted file mode 100644 index 13a599e71..000000000 --- a/livekit-datatrack/src/dtp/common.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2025 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. - -#[derive(Clone)] -pub struct Encryption<'a> { - pub(crate) key_index: u8, - pub(crate) iv: &'a Iv, -} - -impl<'a> Encryption<'a> { - pub fn key_index(&self) -> u8 { - self.key_index - } - pub fn iv(&self) -> &Iv { - self.iv - } -} - -/// AES initialization vector. -pub type Iv = [u8; consts::E2EE_EXT_IV_LEN]; - -pub(crate) mod consts { - pub const SUPPORTED_VERSION: u8 = 0; - pub const BASE_HEADER_LEN: usize = 8; - - // Bitfield shifts and masks for header flags - pub const VERSION_SHIFT: u8 = 5; - pub const VERSION_MASK: u8 = 0xE0; - pub const FINAL_FLAG_SHIFT: u8 = 4; - pub const FINAL_FLAG_MASK: u8 = 0x10; - pub const E2EE_FLAG_SHIFT: u8 = 3; - pub const E2EE_FLAG_MASK: u8 = 0x08; - pub const TS_FLAG_SHIFT: u8 = 2; - pub const TS_FLAG_MASK: u8 = 0x04; - pub const UTS_FLAG_SHIFT: u8 = 1; - pub const UTS_FLAG_MASK: u8 = 0x02; - - // Header field offsets and lengths - pub const EXT_WORDS_OFFSET: usize = 3; - pub const EXT_WORDS_LEN: usize = 1; - pub const TRACK_HANDLE_OFFSET: usize = 4; - pub const TRACK_HANDLE_LEN: usize = 2; - pub const SEQUENCE_OFFSET: usize = 6; - pub const SEQUENCE_LEN: usize = 2; - - // Start of extension fields - pub const EXT_START_OFFSET: usize = BASE_HEADER_LEN; - - // Extension lengths - pub const TIMESTAMP_EXT_LEN: usize = 4; - pub const USER_TIMESTAMP_EXT_LEN: usize = 8; - pub const E2EE_EXT_LEN: usize = E2EE_EXT_IV_LEN + 3 + E2EE_EXT_KEY_INDEX_LEN; - - // E2EE offsets and lengths - pub const E2EE_EXT_IV_LEN: usize = 12; - pub const E2EE_EXT_KEY_INDEX_LEN: usize = 1; - pub const E2EE_EXT_KEY_INDEX_OFFSET: usize = 15; -} diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs new file mode 100644 index 000000000..e662e777e --- /dev/null +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -0,0 +1,264 @@ +// Copyright 2025 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 crate::{ + packet::{Dtp, E2ee, Header, consts::*}, + track_handle::{TrackHandle, TrackHandleError}, +}; +use bytes::{Buf, Bytes}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DeserializeError { + #[error("too short to contain a valid header")] + TooShort, + + #[error("header exceeds total packet length")] + HeaderOverrun, + + #[error("unsupported version {0}")] + UnsupportedVersion(u8), + + #[error("invalid track handle: {0}")] + InvalidTrackHandle(#[from] TrackHandleError), + + #[error("extension with id {0} is malformed")] + MalformedExt(u8), + + #[error("{0} is not a valid extension id")] + InvalidExtId(u8), +} + +#[derive(Debug, Default)] +struct Extensions { + user_timestamp: Option, + e2ee: Option, +} + +impl Dtp { + pub fn deserialize(mut raw: Bytes) -> Result { + let header = Header::deserialize(&mut raw)?; + let payload_len = raw.remaining(); + let payload = raw.copy_to_bytes(payload_len); + Ok(Self { header, payload }) + } +} + +impl Header { + fn deserialize(raw: &mut impl Buf) -> Result { + if raw.remaining() < BASE_HEADER_LEN { + Err(DeserializeError::TooShort)? + } + let initial = raw.get_u8(); + + let version = initial >> VERSION_SHIFT & VERSION_MASK; + if version > SUPPORTED_VERSION { + Err(DeserializeError::UnsupportedVersion(version))? + } + let is_final = (initial >> FINAL_FLAG_SHIFT & FINAL_FLAG_MASK) > 0; + + let extension_words = raw.get_u8(); + let ext_len = 4 * extension_words as usize; + + let track_handle: TrackHandle = raw.get_u16().try_into()?; + let sequence = raw.get_u16(); + let frame_number = raw.get_u16(); + let timestamp = raw.get_u32(); + + if ext_len > raw.remaining() { + Err(DeserializeError::HeaderOverrun)? + } + let ext_block = raw.copy_to_bytes(ext_len); + let extensions = Extensions::parse(ext_block)?; + + let header = Header { + version, + is_final, + track_handle, + sequence, + frame_number, + timestamp, + user_timestamp: extensions.user_timestamp, + e2ee: extensions.e2ee, + }; + Ok(header) + } +} + +impl Extensions { + fn parse(mut raw: impl Buf) -> Result { + let mut extensions = Self::default(); + while raw.remaining() > 0 { + let initial = raw.get_u8(); + if initial == 0 { + // Skip padding + continue; + } + let ext_id = initial >> 4; + match ext_id { + EXT_ID_E2EE => { + if raw.remaining() < EXT_LEN_E2EE { + Err(DeserializeError::MalformedExt(ext_id))? + } + let key_index = raw.get_u8(); + let mut iv = [0u8; 12]; + raw.copy_to_slice(&mut iv); + extensions.e2ee = E2ee { key_index, iv }.into(); + } + EXT_ID_USER_TIMESTAMP => { + if raw.remaining() < EXT_LEN_USER_TIMESTAMP { + Err(DeserializeError::MalformedExt(ext_id))? + } + extensions.user_timestamp = raw.get_u64().into() + } + EXT_ID_INVALID => Err(DeserializeError::InvalidExtId(EXT_ID_INVALID))?, + _ => { + // Skip over unknown extensions (forward compatible). + let ext_len = ((initial & (0xFF ^ 0xF0)) + 1) as usize; + if raw.remaining() < ext_len { + Err(DeserializeError::MalformedExt(ext_id))? + } + raw.advance(ext_len); + continue; + } + } + } + Ok(extensions) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bytes::{BufMut, BytesMut}; + + /// Returns the simplest valid packet to use in test. + fn valid_packet() -> BytesMut { + let mut raw = BytesMut::zeroed(12); // Base header + raw[3] = 1; // Non-zero track ID + raw + } + + #[test] + fn test_short_buffer() { + let mut raw = valid_packet(); + raw.truncate(11); + + let dtp = Dtp::deserialize(raw.freeze()); + assert!(matches!(dtp, Err(DeserializeError::TooShort))); + } + + #[test] + fn test_header_overrun() { + let mut raw = valid_packet(); + raw[1] = 1; // 1 extension word, would overrun buffer + + let dtp = Dtp::deserialize(raw.freeze()); + assert!(matches!(dtp, Err(DeserializeError::HeaderOverrun))); + } + + #[test] + fn test_unsupported_version() { + let mut raw = valid_packet(); + raw[0] = 0x20; // Version 1 (not supported yet) + + let dtp = Dtp::deserialize(raw.freeze()); + assert!(matches!(dtp, Err(DeserializeError::UnsupportedVersion(1)))); + } + + #[test] + fn test_base_header() { + let mut raw = BytesMut::new(); + raw.put_u8(0x10); // Version 0, final flag set + raw.put_u8(0x0); // No extension words + raw.put_slice(&[0x88, 0x11]); // Track ID + raw.put_slice(&[0x44, 0x22]); // Sequence + raw.put_slice(&[0x44, 0x11]); // Frame number + raw.put_slice(&[0x44, 0x22, 0x11, 0x88]); // Timestamp + + let dtp = Dtp::deserialize(raw.freeze()).unwrap(); + assert_eq!(dtp.header.version, 0); + assert_eq!(dtp.header.is_final, true); + assert_eq!(dtp.header.track_handle, 0x8811.try_into().unwrap()); + assert_eq!(dtp.header.sequence, 0x4422); + assert_eq!(dtp.header.frame_number, 0x4411); + assert_eq!(dtp.header.timestamp, 0x44221188); + assert_eq!(dtp.header.user_timestamp, None); + assert_eq!(dtp.header.e2ee, None); + } + + #[test] + fn test_ext_skips_padding() { + let mut raw = valid_packet(); + raw[1] = 4; // 1 extension word + raw.put_bytes(0x00, 32 * 4); // Padding + Dtp::deserialize(raw.freeze()).unwrap(); + } + + #[test] + fn test_ext_e2ee() { + let mut raw = valid_packet(); + raw[1] = 4; // 4 extension words + raw.put_u8(0x1C); // ID 1, length 12 + raw.put_u8(0xFA); // Key index + raw.put_bytes(0x3C, 12); // IV + raw.put_bytes(0x00, 2); // Padding + + let dtp = Dtp::deserialize(raw.freeze()).unwrap(); + let e2ee = dtp.header.e2ee.unwrap(); + assert_eq!(e2ee.key_index, 0xFA); + assert_eq!(e2ee.iv, [0x3C; 12]); + } + + #[test] + fn test_ext_user_timestamp() { + let mut raw = valid_packet(); + raw[1] = 3; // 3 extension words + raw.put_u8(0x27); // ID 2, length 7 + raw.put_slice(&[0x44, 0x11, 0x22, 0x11, 0x11, 0x11, 0x88, 0x11]); // User timestamp + raw.put_bytes(0x00, 3); // Padding + // TODO: decreasing to 2 is header overrun (should be padding error) + let dtp = Dtp::deserialize(raw.freeze()).unwrap(); + assert_eq!(dtp.header.user_timestamp, Some(0x4411221111118811)); + } + + #[test] + fn test_ext_unknown() { + let mut raw = valid_packet(); + raw[1] = 1; // 1 extension word + raw.put_u8(0x80); // ID 8 (unknown) + raw.put_bytes(0x00, 3); // Padding + Dtp::deserialize(raw.freeze()).expect("Should skip unknown extension"); + } + + #[test] + fn test_ext_id_invalid() { + let mut raw = valid_packet(); + raw[1] = 1; // 1 extension word + raw.put_u8(0xF0); // ID 15, invalid + raw.put_bytes(0x00, 3); // Padding + + let dtp = Dtp::deserialize(raw.freeze()); + assert!(matches!(dtp, Err(DeserializeError::InvalidExtId(15)))); + } + + #[test] + fn test_ext_required_word_alignment() { + let mut raw = valid_packet(); + raw[1] = 1; // 1 extension word + raw.put_bytes(0x00, 3); // Padding, missing one byte + + assert!(Dtp::deserialize(raw.freeze()).is_err()); + } +} diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/lib.rs similarity index 80% rename from livekit-datatrack/src/dtp/mod.rs rename to livekit-datatrack/src/dtp/lib.rs index c0b408b24..fd82f7537 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/lib.rs @@ -12,10 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod builder; -mod common; -mod parse; - -pub use builder::{DtpBuilder, BuildError}; -pub use parse::{Dtp, ParseError}; -pub use common::{Encryption, Iv}; \ No newline at end of file +mod packet; +mod track_handle; +mod deserialize; +mod serialize; \ No newline at end of file diff --git a/livekit-datatrack/src/dtp/packet.rs b/livekit-datatrack/src/dtp/packet.rs new file mode 100644 index 000000000..468e0a3b5 --- /dev/null +++ b/livekit-datatrack/src/dtp/packet.rs @@ -0,0 +1,87 @@ +// Copyright 2025 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 crate::track_handle::TrackHandle; +use bytes::Bytes; +use core::fmt; + +#[derive(Clone)] +pub struct Dtp { + pub header: Header, + pub payload: Bytes, +} + +#[derive(Debug, Clone)] +pub struct Header { + pub version: u8, + pub is_final: bool, + pub track_handle: TrackHandle, + pub sequence: u16, + pub frame_number: u16, + pub timestamp: u32, + pub user_timestamp: Option, + pub e2ee: Option, +} + +#[derive(Clone, PartialEq, Eq)] +pub struct E2ee { + pub key_index: u8, + pub iv: [u8; 12], +} + +impl fmt::Debug for Dtp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Dtp") + .field("header", &self.header) + .field("payload_len", &self.payload.len()) + .finish() + } +} + +impl fmt::Debug for E2ee { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // For security, do not include fields in debug. + f.debug_struct("E2ee").finish() + } +} + +/// Constants used in sterilization and deserialization. +pub(crate) mod consts { + pub const SUPPORTED_VERSION: u8 = 0; + pub const BASE_HEADER_LEN: usize = 12; + + // Bitfield shifts and masks for header flags + pub const VERSION_SHIFT: u8 = 5; + pub const VERSION_MASK: u8 = 0x07; + pub const FINAL_FLAG_SHIFT: u8 = 4; + pub const FINAL_FLAG_MASK: u8 = 0x01; + + // Extension IDs + pub const EXT_ID_E2EE: u8 = 0x1; + pub const EXT_ID_USER_TIMESTAMP: u8 = 0x2; + pub const EXT_ID_INVALID: u8 = 0xF; + + // Extension lengths + pub const EXT_LEN_E2EE: usize = 13; + pub const EXT_LEN_USER_TIMESTAMP: usize = 8; + + // Extension markers + pub const EXT_MARKER_LEN: usize = 1; + pub const EXT_MARKER_E2EE: u8 = ext_marker(EXT_ID_E2EE, EXT_LEN_E2EE as u8); + pub const EXT_MARKER_USER_TIMESTAMP: u8 = ext_marker(EXT_ID_USER_TIMESTAMP, EXT_LEN_USER_TIMESTAMP as u8); + + const fn ext_marker(id: u8, len: u8) -> u8 { + (id << 4) | len - 1 + } +} diff --git a/livekit-datatrack/src/dtp/parse.rs b/livekit-datatrack/src/dtp/parse.rs deleted file mode 100644 index c111b250e..000000000 --- a/livekit-datatrack/src/dtp/parse.rs +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright 2025 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 super::common::{Encryption, consts::*}; -use core::fmt; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum ParseError { - #[error("buffer is too short to contain a valid header")] - TooShort, - - #[error("header with length {0} exceeds buffer length")] - HeaderOverrun(usize), -} - -pub struct Dtp<'a> { - buffer: &'a [u8], -} - -/// Extract an integer of type `t` from a byte slice at a given `offset`, -/// interpreting the bytes as big-endian. -macro_rules! extract { - ($t:ty, $bytes:expr, $offset:expr) => {{ - use std::mem::size_of; - let slice: [u8; size_of::<$t>()] = $bytes[$offset..$offset + size_of::<$t>()] - .try_into() - .unwrap(); - <$t>::from_be_bytes(slice) - }}; -} - -impl<'a> Dtp<'a> { - /// Packet version. - pub fn version(&self) -> u8 { - (self.buffer[0] & VERSION_MASK) >> VERSION_SHIFT - } - - /// Whether the packet's final flag is set. - pub fn is_final(&self) -> bool { - ((self.buffer[0] & FINAL_FLAG_MASK) >> FINAL_FLAG_SHIFT) != 0 - } - - /// Whether the packet is encrypted. - pub fn is_encrypted(&self) -> bool { - ((self.buffer[0] & E2EE_FLAG_MASK) >> E2EE_FLAG_SHIFT) != 0 - } - - /// Whether the packet has a timestamp extension. - pub fn has_timestamp(&self) -> bool { - ((self.buffer[0] & TS_FLAG_MASK) >> TS_FLAG_SHIFT) != 0 - } - - /// Whether the packet has a user timestamp extension. - pub fn has_user_timestamp(&self) -> bool { - ((self.buffer[0] & UTS_FLAG_MASK) >> UTS_FLAG_SHIFT) != 0 - } - - /// Total length of all header extensions expressed in number of 32-bit words. - fn extension_words(&self) -> u8 { - self.buffer[EXT_WORDS_OFFSET] - } - - /// Track handle. - pub fn track_handle(&self) -> u16 { - extract!(u16, self.buffer, TRACK_HANDLE_OFFSET) - } - - /// Sequence number. - pub fn sequence(&self) -> u16 { - extract!(u16, self.buffer, SEQUENCE_OFFSET) - } - - /// Timestamp, if the packet has one. - pub fn timestamp(&self) -> Option { - self.has_timestamp() - .then(|| extract!(u32, self.buffer, EXT_START_OFFSET)) - } - - /// User timestamp, if the packet has one. - pub fn user_timestamp(&self) -> Option { - if !self.has_user_timestamp() { - None? - } - let offset = EXT_START_OFFSET - + self - .has_timestamp() - .then_some(TIMESTAMP_EXT_LEN) - .unwrap_or_default(); - extract!(u64, self.buffer, offset).into() - } - - /// Encryption details. - pub fn encryption(&self) -> Option { - if !self.is_encrypted() { - None? - } - let offset = EXT_START_OFFSET - + self - .has_timestamp() - .then_some(TIMESTAMP_EXT_LEN) - .unwrap_or_default() - + self - .has_user_timestamp() - .then_some(USER_TIMESTAMP_EXT_LEN) - .unwrap_or_default(); - let extension = &self.buffer[offset..(offset + E2EE_EXT_LEN)]; - let iv = extension[..E2EE_EXT_IV_LEN].try_into().unwrap(); - let key_index = extension[E2EE_EXT_KEY_INDEX_OFFSET]; - - Encryption { iv, key_index }.into() - } - - /// The payload section of the packet. - pub fn payload(&self) -> &[u8] { - &self.buffer[self.header_len()..] - } - - fn header_len(&self) -> usize { - (4 * self.extension_words() as usize) + BASE_HEADER_LEN - } -} - -impl<'a> TryFrom<&'a [u8]> for Dtp<'a> { - type Error = ParseError; - - fn try_from(buffer: &'a [u8]) -> Result { - if buffer.len() < BASE_HEADER_LEN { - Err(ParseError::TooShort)? - } - let packet = Dtp { buffer }; - if packet.header_len() > buffer.len() { - Err(ParseError::HeaderOverrun(packet.header_len()))? - } - Ok(packet) - } -} - -impl<'a> fmt::Debug for Dtp<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Dtp") - .field("version", &self.version()) - .field("is_final", &self.is_final()) - .field("is_encrypted", &self.is_encrypted()) - .field("track_handle", &self.track_handle()) - .field("sequence", &self.sequence()) - .field("timestamp", &self.timestamp()) - .field("user_timestamp", &self.user_timestamp()) - .field("payload_len", &self.payload().len()) - .finish() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_short_buffer() { - let buffer = [0xFF; 4]; - let dtp: Result = buffer.as_ref().try_into(); - assert!(matches!(dtp, Err(ParseError::TooShort))); - } - - #[test] - fn test_header_overrun() { - let buffer = [ - 0x00, 0x00, 0x00, 0x01, // 1 extension word, would overrun buffer - 0x00, 0x00, 0x00, 0x00, - ]; - let dtp: Result = buffer.as_ref().try_into(); - assert!(matches!(dtp, Err(ParseError::HeaderOverrun(12)))); - } - - #[test] - fn test_field_accessors() { - let buffer = [ - 0x1E, 0x00, // Version 0 | E2EE | Timestamp | User Timestamp - 0x00, 0x07, // Extension Words - 0x04, 0xAB, // Track Handle - 0x04, 0xD2, // Sequence - 0x01, 0x02, // Timestamp - 0x08, 0x40, // ... - 0x01, 0x00, // User Timestamp - 0x01, 0x00, // ... - 0x01, 0x00, // ... - 0x01, 0x00, // ... - 0xFA, 0xFA, // E2EE - 0xFA, 0xFA, // ... - 0xFA, 0xFA, // ... - 0xFA, 0xFA, // ... - 0xFA, 0xFA, // ... - 0xFA, 0xFA, // ... - 0x00, 0x00, // Padding - 0x00, 0x48, // Key Index - 0xFA, 0xAF, // Payload - ]; - - let dtp: Dtp = buffer.as_ref().try_into().unwrap(); - assert_eq!(dtp.version(), 0); - assert!(dtp.is_final()); - assert!(dtp.is_encrypted()); - assert!(dtp.has_timestamp()); - assert!(dtp.has_user_timestamp()); - assert_eq!(dtp.track_handle(), 1195); - assert_eq!(dtp.sequence(), 1234); - assert_eq!(dtp.extension_words(), 7); - assert_eq!(dtp.timestamp(), Some(16_910_400)); - assert_eq!(dtp.user_timestamp(), Some(72_058_693_566_333_184)); - - let encryption = dtp.encryption().unwrap(); - assert_eq!(encryption.key_index(), 72); - assert_eq!(encryption.iv(), &[0xFA; 12]); - - assert_eq!(dtp.payload(), &[0xFA, 0xAF]); - println!("{:?}", dtp); - } -} diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/dtp/serialize.rs new file mode 100644 index 000000000..cdd5c22cf --- /dev/null +++ b/livekit-datatrack/src/dtp/serialize.rs @@ -0,0 +1,201 @@ +// Copyright 2025 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 crate::packet::{Dtp, Header, consts::*}; +use bytes::{BufMut, Bytes, BytesMut}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum SerializeError { + #[error("buffer cannot fit header")] + TooSmallForHeader, + + #[error("buffer cannot fit payload")] + TooSmallForPayload, +} + +impl Dtp { + /// Serialize the packet into a new buffer. + pub fn serialize(self) -> Bytes { + let len = self.serialized_len(); + let mut buf = BytesMut::with_capacity(len); + + let written = self.serialize_into(&mut buf).unwrap(); + assert_eq!(written, len); + buf.freeze() + } + + /// Serialize the packet into the given buffer. + /// + /// If the given buffer is too short to accommodate the serialized packet, the result + /// is an error. Use [`Self::serialized_len()`] to get the required buffer size. + /// + pub fn serialize_into(self, buf: &mut impl BufMut) -> Result { + let payload_len = self.payload.len(); + let header_len = self.header.serialize_into(buf)?; + if buf.remaining_mut() < self.payload.len() { + Err(SerializeError::TooSmallForPayload)? + } + buf.put(self.payload); + Ok(header_len + payload_len) + } +} + +impl Dtp { + /// Length of the serialized packet in bytes. + pub fn serialized_len(&self) -> usize { + self.header.serialized_len() + self.payload.len() + } +} + +impl Header { + fn serialize_into(self, buf: &mut impl BufMut) -> Result { + let metrics = self.metrics(); + if buf.remaining_mut() < metrics.len { + Err(SerializeError::TooSmallForHeader)? + } + + let mut initial = SUPPORTED_VERSION << VERSION_SHIFT; + if self.is_final { + initial |= 1 << FINAL_FLAG_SHIFT; + } + buf.put_u8(initial); + buf.put_u8(metrics.ext_words as u8); + + buf.put_u16(self.track_handle.into()); + buf.put_u16(self.sequence); + buf.put_u16(self.frame_number); + buf.put_u32(self.timestamp); + + if let Some(e2ee) = &self.e2ee { + buf.put_u8(EXT_MARKER_E2EE); + buf.put_u8(e2ee.key_index); + buf.put_slice(&e2ee.iv); + } + if let Some(user_timestamp) = self.user_timestamp { + buf.put_u8(EXT_MARKER_USER_TIMESTAMP); + buf.put_u64(user_timestamp); + } + buf.put_bytes(0, metrics.padding_len); + + Ok(metrics.len) + } +} + +#[derive(Debug)] +struct HeaderMetrics { + /// Number of 32-bit extension words. + ext_words: usize, + /// Number of padding bytes needed to align extension block. + padding_len: usize, + /// Total size of the serialized header in bytes + len: usize, +} + +impl Header { + /// Length of the serialized header in bytes. + fn serialized_len(&self) -> usize { + self.metrics().len + } + + /// Length of all extensions not including padding. + fn ext_len(&self) -> usize { + let mut len = 0; + if self.e2ee.is_some() { + len += EXT_MARKER_LEN + EXT_LEN_E2EE; + } + if self.user_timestamp.is_some() { + len += EXT_MARKER_LEN + EXT_LEN_USER_TIMESTAMP; + } + len + } + + /// Header metrics required for buffer sizing and serialization. + fn metrics(&self) -> HeaderMetrics { + let ext_len = self.ext_len(); + let ext_words = ext_len.div_ceil(4); + assert!(ext_words <= u8::MAX.into()); + let padding_len = (ext_words as usize * 4) - ext_len; + let len = BASE_HEADER_LEN + ext_len + padding_len; + HeaderMetrics { + ext_words, + padding_len, + len, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::packet::E2ee; + use bytes::Buf; + + /// Constructed packet to use in tests. + fn packet() -> Dtp { + Dtp { + header: Header { + version: 0, + is_final: true, + track_handle: 0x8811.try_into().unwrap(), + sequence: 0x4422, + frame_number: 0x4411, + timestamp: 0x44221188, + user_timestamp: 0x4411221111118811.into(), + e2ee: E2ee { + key_index: 0xFA, + iv: [0x3C; 12], + } + .into(), + }, + payload: vec![0xFA; 1024].into(), + } + } + + #[test] + fn test_header_metrics() { + let metrics = packet().header.metrics(); + assert_eq!(metrics.ext_words, 6); + assert_eq!(metrics.padding_len, 1); + assert_eq!(metrics.len, 36); + } + + #[test] + fn test_serialize() { + let mut buf = packet().serialize().try_into_mut().unwrap(); + assert_eq!(buf.len(), 1060); + + // Base header + assert_eq!(buf.get_u8(), 0x10); // Version 0, final flag set + assert_eq!(buf.get_u8(), 6); // Extension words + assert_eq!(buf.get_u16(), 0x8811); // Track handle + assert_eq!(buf.get_u16(), 0x4422); // Sequence + assert_eq!(buf.get_u16(), 0x4411); // Frame number + assert_eq!(buf.get_u32(), 0x44221188); // Timestamp + + // E2EE extension + assert_eq!(buf.get_u8(), 0x1C); // ID 1, length 12 + assert_eq!(buf.get_u8(), 0xFA); // Key index + assert_eq!(buf.copy_to_bytes(12), vec![0x3C; 12]); + + // User timestamp extension + assert_eq!(buf.get_u8(), 0x27); // ID 2, length 7 + assert_eq!(buf.get_u64(), 0x4411221111118811); + + assert_eq!(buf.get_u8(), 0); // Padding + assert_eq!(buf.copy_to_bytes(1024), vec![0xFA; 1024]); // Payload + + assert_eq!(buf.remaining(), 0); + } +} diff --git a/livekit-datatrack/src/dtp/track_handle.rs b/livekit-datatrack/src/dtp/track_handle.rs new file mode 100644 index 000000000..9bf7a1724 --- /dev/null +++ b/livekit-datatrack/src/dtp/track_handle.rs @@ -0,0 +1,49 @@ +// Copyright 2025 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 std::fmt::Display; +use thiserror::Error; + +/// Handle identifying a data track at the transport level. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TrackHandle(u16); + +#[derive(Debug, Error)] +pub enum TrackHandleError { + #[error("{0:#X} is reserved")] + Reserved(u16), +} + +impl TryFrom for TrackHandle { + type Error = TrackHandleError; + + fn try_from(value: u16) -> Result { + if value == 0 { + Err(TrackHandleError::Reserved(value))? + } + Ok(Self(value)) + } +} + +impl Into for TrackHandle { + fn into(self) -> u16 { + self.0 + } +} + +impl Display for TrackHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{0:#X}", self.0) + } +} From 478ceca7daf73d2f5646fbe59a1e9f52e2c980a5 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:28:23 +1100 Subject: [PATCH 007/232] Update protocol --- livekit-protocol/protocol | 2 +- livekit-protocol/src/livekit.rs | 232 ++-- livekit-protocol/src/livekit.serde.rs | 1769 ++++++++++++++++++------- 3 files changed, 1428 insertions(+), 575 deletions(-) diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index c357756e3..2a0a69c0d 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit c357756e39c7b23364e328cdd1619483ef5e68ef +Subproject commit 2a0a69c0d7a863a888f0da003d79558db7411358 diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index 6248bd7eb..992dcb0b6 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -409,6 +409,10 @@ pub mod participant_info { Sip = 3, /// LiveKit agents Agent = 4, + /// Connectors participants + /// + /// NEXT_ID: 8 + Connector = 7, } impl Kind { /// String value of the enum field names used in the ProtoBuf definition. @@ -422,6 +426,7 @@ pub mod participant_info { Kind::Egress => "EGRESS", Kind::Sip => "SIP", Kind::Agent => "AGENT", + Kind::Connector => "CONNECTOR", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -432,6 +437,7 @@ pub mod participant_info { "EGRESS" => Some(Self::Egress), "SIP" => Some(Self::Sip), "AGENT" => Some(Self::Agent), + "CONNECTOR" => Some(Self::Connector), _ => None, } } @@ -586,23 +592,20 @@ pub struct TrackInfo { pub struct DataTrackInfo { #[prost(string, tag="1")] pub sid: ::prost::alloc::string::String, - /// 16-bit, SFU-assigned identifier used to associate packets with the track, unique per room. - #[prost(uint32, tag="2")] - pub handle: u32, /// Human-readable identifier (e.g., `geoLocation`, `servoPosition.x`, etc.), unique per publisher. - #[prost(string, tag="3")] + #[prost(string, tag="2")] pub name: ::prost::alloc::string::String, /// MIME type of the data sent over the track (e.g., `application/json`). /// This must be a valid MIME type as defined by RFC 2046. - #[prost(string, tag="4")] + #[prost(string, tag="3")] pub mime_type: ::prost::alloc::string::String, /// Method used for end-to-end encryption (E2EE) on packet payloads. - #[prost(enumeration="encryption::Type", tag="5")] + #[prost(enumeration="encryption::Type", tag="4")] pub encryption: i32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct DataTrackSubscribeOptions { +pub struct DataTrackSubscriptionOptions { /// Rate in frames per second (FPS) the subscriber wants to receive frames at. /// If omitted, the subscriber defaults to the publisher's nominal FPS; if the /// publisher has no nominal FPS, it will use the maximum. @@ -3056,7 +3059,7 @@ impl EgressSourceType { #[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")] + #[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")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalRequest`. @@ -3117,16 +3120,19 @@ pub mod signal_request { UpdateVideoTrack(super::UpdateLocalVideoTrack), /// Publish a data track #[prost(message, tag="19")] - AddDataTrack(super::AddDataTrackRequest), - /// Update subscription state for one or more data tracks + PublishDataTrack(super::PublishDataTrackRequest), + /// Unpublish a data track #[prost(message, tag="20")] + UnpublishDataTrack(super::UnpublishDataTrackRequest), + /// Update subscription state for one or more data tracks + #[prost(message, tag="21")] UpdateDataSubscription(super::UpdateDataSubscription), } } #[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")] + #[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")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalResponse`. @@ -3212,9 +3218,15 @@ pub mod signal_response { /// when audio subscription changes, used to enable simulcasting of audio codecs based on subscriptions #[prost(message, tag="26")] SubscribedAudioCodecUpdate(super::SubscribedAudioCodecUpdate), - /// Sent in response to `AddDataTrackRequest`. + /// Sent in response to `PublishDataTrackRequest`. #[prost(message, tag="27")] - DataTrackPublished(super::DataTrackPublishedResponse), + PublishDataTrack(super::PublishDataTrackResponse), + /// Sent in response to `UnpublishDataTrackRequest` or SFU-initiated unpublish. + #[prost(message, tag="28")] + UnpublishDataTrack(super::UnpublishDataTrackResponse), + /// Sent to data track subscribers to provide mapping from track SIDs to handles. + #[prost(message, tag="29")] + DataTrackHandles(super::DataTrackSubscriberHandles), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -3281,74 +3293,51 @@ pub struct AddTrackRequest { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct AddDataTrackRequest { +pub struct PublishDataTrackRequest { + /// 16-bit identifier attached to packets, unique per publisher. + #[prost(uint32, tag="1")] + pub handle: u32, /// Human-readable identifier (e.g., `geoLocation`, `servoPosition.x`, etc.), unique per publisher. - #[prost(string, tag="1")] + #[prost(string, tag="2")] pub name: ::prost::alloc::string::String, /// MIME type of the data sent over the track (e.g., `application/json`). /// This must be a valid MIME type as defined by RFC 2046. - #[prost(string, tag="2")] + #[prost(string, tag="3")] pub mime_type: ::prost::alloc::string::String, /// Method used for end-to-end encryption (E2EE) on frame payloads. - #[prost(enumeration="encryption::Type", tag="3")] + #[prost(enumeration="encryption::Type", tag="4")] pub encryption: i32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct DataTrackPublishedResponse { +pub struct PublishDataTrackResponse { + /// Handle of the track that was published. #[prost(uint32, tag="1")] - pub request_id: u32, - #[prost(oneof="data_track_published_response::Result", tags="2, 3")] - pub result: ::core::option::Option, + pub handle: u32, + /// Information about the published track. + #[prost(message, optional, tag="2")] + pub info: ::core::option::Option, } -/// Nested message and enum types in `DataTrackPublishedResponse`. -pub mod data_track_published_response { - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] - #[repr(i32)] - pub enum Error { - /// Unknown reason. - Unknown = 0, - /// Name is invalid. - InvalidName = 1, - /// MIME type is invalid. - InvalidMimeType = 2, - /// Publisher already has a data track published with the same name. - NameTaken = 3, - } - impl Error { - /// 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 { - Error::Unknown => "UNKNOWN", - Error::InvalidName => "INVALID_NAME", - Error::InvalidMimeType => "INVALID_MIME_TYPE", - Error::NameTaken => "NAME_TAKEN", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "UNKNOWN" => Some(Self::Unknown), - "INVALID_NAME" => Some(Self::InvalidName), - "INVALID_MIME_TYPE" => Some(Self::InvalidMimeType), - "NAME_TAKEN" => Some(Self::NameTaken), - _ => None, - } - } - } - #[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Result { - /// Track was successfully published, track info is provided. - #[prost(message, tag="2")] - Ok(super::DataTrackInfo), - /// Track could not be published, reason is provided. - #[prost(enumeration="Error", tag="3")] - Error(i32), - } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UnpublishDataTrackRequest { + /// Handle of the track to unpublish. + #[prost(uint32, tag="1")] + pub handle: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UnpublishDataTrackResponse { + /// Handle of the track that was unpublished. + #[prost(uint32, tag="1")] + pub handle: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackSubscriberHandles { + /// Mapping from data track SIDs to handles subscribers will see on incoming packets. + #[prost(map="string, uint32", tag="1")] + pub handles: ::std::collections::HashMap<::prost::alloc::string::String, u32>, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3446,6 +3435,8 @@ pub struct SessionDescription { pub sdp: ::prost::alloc::string::String, #[prost(uint32, tag="3")] pub id: u32, + #[prost(map="string, string", tag="4")] + pub mid_to_track_id: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3466,14 +3457,23 @@ pub struct UpdateSubscription { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct UpdateDataSubscription { - #[prost(string, repeated, tag="1")] - pub track_sids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - #[prost(bool, tag="2")] - pub subscribe: bool, - /// Options for each track subscription. Entries in this list align - /// positionally with `track_sids`. - #[prost(message, repeated, tag="3")] - pub options: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="1")] + pub updates: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `UpdateDataSubscription`. +pub mod update_data_subscription { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] + pub struct Update { + #[prost(string, tag="1")] + pub sid: ::prost::alloc::string::String, + #[prost(bool, tag="2")] + pub subscribe: bool, + /// Options to apply when initially subscribing or updating an existing subscription. + /// When unsubscribing, this field is ignored. + #[prost(message, optional, tag="3")] + pub options: ::core::option::Option, + } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3864,7 +3864,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")] + #[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`. @@ -3879,6 +3879,10 @@ pub mod request_response { Queued = 4, UnsupportedType = 5, UnclassifiedError = 6, + DataTrackInvalidHandle = 7, + DataTrackInvalidName = 8, + DataTrackInvalidMimeType = 9, + DataTrackNameTaken = 10, } impl Reason { /// String value of the enum field names used in the ProtoBuf definition. @@ -3894,6 +3898,10 @@ pub mod request_response { Reason::Queued => "QUEUED", Reason::UnsupportedType => "UNSUPPORTED_TYPE", Reason::UnclassifiedError => "UNCLASSIFIED_ERROR", + Reason::DataTrackInvalidHandle => "DATA_TRACK_INVALID_HANDLE", + Reason::DataTrackInvalidName => "DATA_TRACK_INVALID_NAME", + Reason::DataTrackInvalidMimeType => "DATA_TRACK_INVALID_MIME_TYPE", + Reason::DataTrackNameTaken => "DATA_TRACK_NAME_TAKEN", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -3906,6 +3914,10 @@ pub mod request_response { "QUEUED" => Some(Self::Queued), "UNSUPPORTED_TYPE" => Some(Self::UnsupportedType), "UNCLASSIFIED_ERROR" => Some(Self::UnclassifiedError), + "DATA_TRACK_INVALID_HANDLE" => Some(Self::DataTrackInvalidHandle), + "DATA_TRACK_INVALID_NAME" => Some(Self::DataTrackInvalidName), + "DATA_TRACK_INVALID_MIME_TYPE" => Some(Self::DataTrackInvalidMimeType), + "DATA_TRACK_NAME_TAKEN" => Some(Self::DataTrackNameTaken), _ => None, } } @@ -3925,6 +3937,10 @@ pub mod request_response { UpdateAudioTrack(super::UpdateLocalAudioTrack), #[prost(message, tag="9")] UpdateVideoTrack(super::UpdateLocalVideoTrack), + #[prost(message, tag="10")] + PublishDataTrack(super::PublishDataTrackRequest), + #[prost(message, tag="11")] + UnpublishDataTrack(super::UnpublishDataTrackRequest), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -5274,6 +5290,18 @@ pub struct CreateSipTrunkRequest { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct ProviderInfo { + #[prost(string, tag="1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub name: ::prost::alloc::string::String, + #[prost(enumeration="ProviderType", tag="3")] + pub r#type: i32, + #[prost(bool, tag="4")] + pub prevent_transfer: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct SipTrunkInfo { #[prost(string, tag="1")] pub sip_trunk_id: ::prost::alloc::string::String, @@ -5939,10 +5967,11 @@ pub struct CreateSipParticipantRequest { /// 1) Unspecified: Use legacy behavior - display name will be set to be the caller's number. /// 2) Empty string: Do not send a display name, which will result in a CNAM lookup downstream. /// 3) Non-empty: Use the specified value as the display name. - /// - /// NEXT ID: 22 #[prost(string, optional, tag="21")] pub display_name: ::core::option::Option<::prost::alloc::string::String>, + /// NEXT ID: 23 + #[prost(message, optional, tag="22")] + pub destination: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -6034,6 +6063,8 @@ pub struct SipCallInfo { pub pcap_file_link: ::prost::alloc::string::String, #[prost(message, repeated, tag="26")] pub call_context: ::prost::alloc::vec::Vec<::pbjson_types::Any>, + #[prost(message, optional, tag="27")] + pub provider_info: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -6069,6 +6100,16 @@ pub struct SipUri { #[prost(enumeration="SipTransport", tag="5")] pub transport: i32, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Destination { + #[prost(string, tag="1")] + pub city: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub country: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub region: ::prost::alloc::string::String, +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum SipStatusCode { @@ -6334,6 +6375,37 @@ impl SipMediaEncryption { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] +pub enum ProviderType { + Unknown = 0, + /// Internally implemented + Internal = 1, + /// Vendor provided + External = 2, +} +impl ProviderType { + /// 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 { + ProviderType::Unknown => "PROVIDER_TYPE_UNKNOWN", + ProviderType::Internal => "PROVIDER_TYPE_INTERNAL", + ProviderType::External => "PROVIDER_TYPE_EXTERNAL", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "PROVIDER_TYPE_UNKNOWN" => Some(Self::Unknown), + "PROVIDER_TYPE_INTERNAL" => Some(Self::Internal), + "PROVIDER_TYPE_EXTERNAL" => Some(Self::External), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] pub enum SipCallStatus { /// Incoming call is being handled by the SIP service. The SIP participant hasn't joined a LiveKit room yet ScsCallIncoming = 0, diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index 44b89de26..7cd6cfd8c 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -94,138 +94,6 @@ impl<'de> serde::Deserialize<'de> for ActiveSpeakerUpdate { deserializer.deserialize_struct("livekit.ActiveSpeakerUpdate", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for AddDataTrackRequest { - #[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.mime_type.is_empty() { - len += 1; - } - if self.encryption != 0 { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.AddDataTrackRequest", len)?; - if !self.name.is_empty() { - struct_ser.serialize_field("name", &self.name)?; - } - if !self.mime_type.is_empty() { - struct_ser.serialize_field("mimeType", &self.mime_type)?; - } - if self.encryption != 0 { - let v = encryption::Type::try_from(self.encryption) - .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; - struct_ser.serialize_field("encryption", &v)?; - } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for AddDataTrackRequest { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "name", - "mime_type", - "mimeType", - "encryption", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - Name, - MimeType, - Encryption, - __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), - "mimeType" | "mime_type" => Ok(GeneratedField::MimeType), - "encryption" => Ok(GeneratedField::Encryption), - _ => Ok(GeneratedField::__SkipField__), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = AddDataTrackRequest; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.AddDataTrackRequest") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut name__ = None; - let mut mime_type__ = None; - let mut encryption__ = 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::MimeType => { - if mime_type__.is_some() { - return Err(serde::de::Error::duplicate_field("mimeType")); - } - mime_type__ = Some(map_.next_value()?); - } - GeneratedField::Encryption => { - if encryption__.is_some() { - return Err(serde::de::Error::duplicate_field("encryption")); - } - encryption__ = Some(map_.next_value::()? as i32); - } - GeneratedField::__SkipField__ => { - let _ = map_.next_value::()?; - } - } - } - Ok(AddDataTrackRequest { - name: name__.unwrap_or_default(), - mime_type: mime_type__.unwrap_or_default(), - encryption: encryption__.unwrap_or_default(), - }) - } - } - deserializer.deserialize_struct("livekit.AddDataTrackRequest", FIELDS, GeneratedVisitor) - } -} impl serde::Serialize for AddTrackRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -4897,6 +4765,9 @@ impl serde::Serialize for CreateSipParticipantRequest { if self.display_name.is_some() { len += 1; } + if self.destination.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.CreateSIPParticipantRequest", len)?; if !self.sip_trunk_id.is_empty() { struct_ser.serialize_field("sipTrunkId", &self.sip_trunk_id)?; @@ -4965,6 +4836,9 @@ impl serde::Serialize for CreateSipParticipantRequest { if let Some(v) = self.display_name.as_ref() { struct_ser.serialize_field("displayName", v)?; } + if let Some(v) = self.destination.as_ref() { + struct_ser.serialize_field("destination", v)?; + } struct_ser.end() } } @@ -5014,6 +4888,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { "waitUntilAnswered", "display_name", "displayName", + "destination", ]; #[allow(clippy::enum_variant_names)] @@ -5039,6 +4914,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { MediaEncryption, WaitUntilAnswered, DisplayName, + Destination, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -5082,6 +4958,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { "mediaEncryption" | "media_encryption" => Ok(GeneratedField::MediaEncryption), "waitUntilAnswered" | "wait_until_answered" => Ok(GeneratedField::WaitUntilAnswered), "displayName" | "display_name" => Ok(GeneratedField::DisplayName), + "destination" => Ok(GeneratedField::Destination), _ => Ok(GeneratedField::__SkipField__), } } @@ -5122,6 +4999,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { let mut media_encryption__ = None; let mut wait_until_answered__ = None; let mut display_name__ = None; + let mut destination__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::SipTrunkId => { @@ -5254,6 +5132,12 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { } display_name__ = map_.next_value()?; } + GeneratedField::Destination => { + if destination__.is_some() { + return Err(serde::de::Error::duplicate_field("destination")); + } + destination__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -5281,6 +5165,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { media_encryption: media_encryption__.unwrap_or_default(), wait_until_answered: wait_until_answered__.unwrap_or_default(), display_name: display_name__, + destination: destination__, }) } } @@ -7224,9 +7109,6 @@ impl serde::Serialize for DataTrackInfo { if !self.sid.is_empty() { len += 1; } - if self.handle != 0 { - len += 1; - } if !self.name.is_empty() { len += 1; } @@ -7240,9 +7122,6 @@ impl serde::Serialize for DataTrackInfo { if !self.sid.is_empty() { struct_ser.serialize_field("sid", &self.sid)?; } - if self.handle != 0 { - struct_ser.serialize_field("handle", &self.handle)?; - } if !self.name.is_empty() { struct_ser.serialize_field("name", &self.name)?; } @@ -7265,7 +7144,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { { const FIELDS: &[&str] = &[ "sid", - "handle", "name", "mime_type", "mimeType", @@ -7275,7 +7153,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { #[allow(clippy::enum_variant_names)] enum GeneratedField { Sid, - Handle, Name, MimeType, Encryption, @@ -7302,7 +7179,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { { match value { "sid" => Ok(GeneratedField::Sid), - "handle" => Ok(GeneratedField::Handle), "name" => Ok(GeneratedField::Name), "mimeType" | "mime_type" => Ok(GeneratedField::MimeType), "encryption" => Ok(GeneratedField::Encryption), @@ -7326,7 +7202,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { V: serde::de::MapAccess<'de>, { let mut sid__ = None; - let mut handle__ = None; let mut name__ = None; let mut mime_type__ = None; let mut encryption__ = None; @@ -7338,14 +7213,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { } sid__ = Some(map_.next_value()?); } - GeneratedField::Handle => { - if handle__.is_some() { - return Err(serde::de::Error::duplicate_field("handle")); - } - handle__ = - Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) - ; - } GeneratedField::Name => { if name__.is_some() { return Err(serde::de::Error::duplicate_field("name")); @@ -7371,7 +7238,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { } Ok(DataTrackInfo { sid: sid__.unwrap_or_default(), - handle: handle__.unwrap_or_default(), name: name__.unwrap_or_default(), mime_type: mime_type__.unwrap_or_default(), encryption: encryption__.unwrap_or_default(), @@ -7381,7 +7247,7 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { deserializer.deserialize_struct("livekit.DataTrackInfo", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DataTrackPublishedResponse { +impl serde::Serialize for DataTrackSubscriberHandles { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -7389,49 +7255,29 @@ impl serde::Serialize for DataTrackPublishedResponse { { use serde::ser::SerializeStruct; let mut len = 0; - if self.request_id != 0 { + if !self.handles.is_empty() { len += 1; } - if self.result.is_some() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.DataTrackPublishedResponse", len)?; - if self.request_id != 0 { - struct_ser.serialize_field("requestId", &self.request_id)?; - } - if let Some(v) = self.result.as_ref() { - match v { - data_track_published_response::Result::Ok(v) => { - struct_ser.serialize_field("ok", v)?; - } - data_track_published_response::Result::Error(v) => { - let v = data_track_published_response::Error::try_from(*v) - .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", *v)))?; - struct_ser.serialize_field("error", &v)?; - } - } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSubscriberHandles", len)?; + if !self.handles.is_empty() { + struct_ser.serialize_field("handles", &self.handles)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DataTrackPublishedResponse { +impl<'de> serde::Deserialize<'de> for DataTrackSubscriberHandles { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "request_id", - "requestId", - "ok", - "error", + "handles", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - RequestId, - Ok, - Error, + Handles, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -7454,9 +7300,7 @@ impl<'de> serde::Deserialize<'de> for DataTrackPublishedResponse { E: serde::de::Error, { match value { - "requestId" | "request_id" => Ok(GeneratedField::RequestId), - "ok" => Ok(GeneratedField::Ok), - "error" => Ok(GeneratedField::Error), + "handles" => Ok(GeneratedField::Handles), _ => Ok(GeneratedField::__SkipField__), } } @@ -7466,133 +7310,42 @@ impl<'de> serde::Deserialize<'de> for DataTrackPublishedResponse { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DataTrackPublishedResponse; + type Value = DataTrackSubscriberHandles; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DataTrackPublishedResponse") + formatter.write_str("struct livekit.DataTrackSubscriberHandles") } - 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 request_id__ = None; - let mut result__ = None; + let mut handles__ = 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::Ok => { - if result__.is_some() { - return Err(serde::de::Error::duplicate_field("ok")); - } - result__ = map_.next_value::<::std::option::Option<_>>()?.map(data_track_published_response::Result::Ok) -; - } - GeneratedField::Error => { - if result__.is_some() { - return Err(serde::de::Error::duplicate_field("error")); + GeneratedField::Handles => { + if handles__.is_some() { + return Err(serde::de::Error::duplicate_field("handles")); } - result__ = map_.next_value::<::std::option::Option>()?.map(|x| data_track_published_response::Result::Error(x as i32)); + handles__ = Some( + map_.next_value::>>()? + .into_iter().map(|(k,v)| (k, v.0)).collect() + ); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(DataTrackPublishedResponse { - request_id: request_id__.unwrap_or_default(), - result: result__, + Ok(DataTrackSubscriberHandles { + handles: handles__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.DataTrackPublishedResponse", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DataTrackSubscriberHandles", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for data_track_published_response::Error { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - let variant = match self { - Self::Unknown => "UNKNOWN", - Self::InvalidName => "INVALID_NAME", - Self::InvalidMimeType => "INVALID_MIME_TYPE", - Self::NameTaken => "NAME_TAKEN", - }; - serializer.serialize_str(variant) - } -} -impl<'de> serde::Deserialize<'de> for data_track_published_response::Error { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "UNKNOWN", - "INVALID_NAME", - "INVALID_MIME_TYPE", - "NAME_TAKEN", - ]; - - struct GeneratedVisitor; - - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = data_track_published_response::Error; - - 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 { - "UNKNOWN" => Ok(data_track_published_response::Error::Unknown), - "INVALID_NAME" => Ok(data_track_published_response::Error::InvalidName), - "INVALID_MIME_TYPE" => Ok(data_track_published_response::Error::InvalidMimeType), - "NAME_TAKEN" => Ok(data_track_published_response::Error::NameTaken), - _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), - } - } - } - deserializer.deserialize_any(GeneratedVisitor) - } -} -impl serde::Serialize for DataTrackSubscribeOptions { +impl serde::Serialize for DataTrackSubscriptionOptions { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -7603,14 +7356,14 @@ impl serde::Serialize for DataTrackSubscribeOptions { if self.target_fps.is_some() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSubscribeOptions", len)?; + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSubscriptionOptions", len)?; if let Some(v) = self.target_fps.as_ref() { struct_ser.serialize_field("targetFps", v)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DataTrackSubscribeOptions { +impl<'de> serde::Deserialize<'de> for DataTrackSubscriptionOptions { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where @@ -7656,13 +7409,13 @@ impl<'de> serde::Deserialize<'de> for DataTrackSubscribeOptions { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DataTrackSubscribeOptions; + type Value = DataTrackSubscriptionOptions; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DataTrackSubscribeOptions") + formatter.write_str("struct livekit.DataTrackSubscriptionOptions") } - 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>, { @@ -7682,12 +7435,12 @@ impl<'de> serde::Deserialize<'de> for DataTrackSubscribeOptions { } } } - Ok(DataTrackSubscribeOptions { + Ok(DataTrackSubscriptionOptions { target_fps: target_fps__, }) } } - deserializer.deserialize_struct("livekit.DataTrackSubscribeOptions", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DataTrackSubscriptionOptions", FIELDS, GeneratedVisitor) } } impl serde::Serialize for DeleteAgentDispatchRequest { @@ -8258,7 +8011,7 @@ impl<'de> serde::Deserialize<'de> for DeleteSipTrunkRequest { deserializer.deserialize_struct("livekit.DeleteSIPTrunkRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DirectFileOutput { +impl serde::Serialize for Destination { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -8266,65 +8019,194 @@ impl serde::Serialize for DirectFileOutput { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.filepath.is_empty() { + if !self.city.is_empty() { len += 1; } - if self.disable_manifest { + if !self.country.is_empty() { len += 1; } - if self.output.is_some() { + if !self.region.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.DirectFileOutput", len)?; - if !self.filepath.is_empty() { - struct_ser.serialize_field("filepath", &self.filepath)?; + let mut struct_ser = serializer.serialize_struct("livekit.Destination", len)?; + if !self.city.is_empty() { + struct_ser.serialize_field("city", &self.city)?; } - if self.disable_manifest { - struct_ser.serialize_field("disableManifest", &self.disable_manifest)?; + if !self.country.is_empty() { + struct_ser.serialize_field("country", &self.country)?; } - if let Some(v) = self.output.as_ref() { - match v { - direct_file_output::Output::S3(v) => { - struct_ser.serialize_field("s3", v)?; - } - direct_file_output::Output::Gcp(v) => { - struct_ser.serialize_field("gcp", v)?; - } - direct_file_output::Output::Azure(v) => { - struct_ser.serialize_field("azure", v)?; - } - direct_file_output::Output::AliOss(v) => { - struct_ser.serialize_field("aliOSS", v)?; - } - } + if !self.region.is_empty() { + struct_ser.serialize_field("region", &self.region)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DirectFileOutput { +impl<'de> serde::Deserialize<'de> for Destination { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "filepath", - "disable_manifest", - "disableManifest", - "s3", - "gcp", - "azure", - "aliOSS", + "city", + "country", + "region", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Filepath, - DisableManifest, - S3, - Gcp, - Azure, - AliOss, + City, + Country, + Region, + __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 { + "city" => Ok(GeneratedField::City), + "country" => Ok(GeneratedField::Country), + "region" => Ok(GeneratedField::Region), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Destination; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.Destination") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut city__ = None; + let mut country__ = None; + let mut region__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::City => { + if city__.is_some() { + return Err(serde::de::Error::duplicate_field("city")); + } + city__ = Some(map_.next_value()?); + } + GeneratedField::Country => { + if country__.is_some() { + return Err(serde::de::Error::duplicate_field("country")); + } + country__ = Some(map_.next_value()?); + } + GeneratedField::Region => { + if region__.is_some() { + return Err(serde::de::Error::duplicate_field("region")); + } + region__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(Destination { + city: city__.unwrap_or_default(), + country: country__.unwrap_or_default(), + region: region__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.Destination", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DirectFileOutput { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.filepath.is_empty() { + len += 1; + } + if self.disable_manifest { + len += 1; + } + if self.output.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DirectFileOutput", len)?; + if !self.filepath.is_empty() { + struct_ser.serialize_field("filepath", &self.filepath)?; + } + if self.disable_manifest { + struct_ser.serialize_field("disableManifest", &self.disable_manifest)?; + } + if let Some(v) = self.output.as_ref() { + match v { + direct_file_output::Output::S3(v) => { + struct_ser.serialize_field("s3", v)?; + } + direct_file_output::Output::Gcp(v) => { + struct_ser.serialize_field("gcp", v)?; + } + direct_file_output::Output::Azure(v) => { + struct_ser.serialize_field("azure", v)?; + } + direct_file_output::Output::AliOss(v) => { + struct_ser.serialize_field("aliOSS", v)?; + } + } + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DirectFileOutput { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "filepath", + "disable_manifest", + "disableManifest", + "s3", + "gcp", + "azure", + "aliOSS", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Filepath, + DisableManifest, + S3, + Gcp, + Azure, + AliOss, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -20204,6 +20086,7 @@ impl serde::Serialize for participant_info::Kind { Self::Egress => "EGRESS", Self::Sip => "SIP", Self::Agent => "AGENT", + Self::Connector => "CONNECTOR", }; serializer.serialize_str(variant) } @@ -20220,6 +20103,7 @@ impl<'de> serde::Deserialize<'de> for participant_info::Kind { "EGRESS", "SIP", "AGENT", + "CONNECTOR", ]; struct GeneratedVisitor; @@ -20265,6 +20149,7 @@ impl<'de> serde::Deserialize<'de> for participant_info::Kind { "EGRESS" => Ok(participant_info::Kind::Egress), "SIP" => Ok(participant_info::Kind::Sip), "AGENT" => Ok(participant_info::Kind::Agent), + "CONNECTOR" => Ok(participant_info::Kind::Connector), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -21321,9 +21206,486 @@ impl<'de> serde::Deserialize<'de> for PlayoutDelay { E: serde::de::Error, { match value { - "enabled" => Ok(GeneratedField::Enabled), - "min" => Ok(GeneratedField::Min), - "max" => Ok(GeneratedField::Max), + "enabled" => Ok(GeneratedField::Enabled), + "min" => Ok(GeneratedField::Min), + "max" => Ok(GeneratedField::Max), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = PlayoutDelay; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.PlayoutDelay") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut enabled__ = None; + let mut min__ = None; + let mut max__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Enabled => { + if enabled__.is_some() { + return Err(serde::de::Error::duplicate_field("enabled")); + } + enabled__ = Some(map_.next_value()?); + } + GeneratedField::Min => { + if min__.is_some() { + return Err(serde::de::Error::duplicate_field("min")); + } + min__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Max => { + if max__.is_some() { + return Err(serde::de::Error::duplicate_field("max")); + } + max__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(PlayoutDelay { + enabled: enabled__.unwrap_or_default(), + min: min__.unwrap_or_default(), + max: max__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.PlayoutDelay", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Pong { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.last_ping_timestamp != 0 { + len += 1; + } + if self.timestamp != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.Pong", len)?; + if self.last_ping_timestamp != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("lastPingTimestamp", ToString::to_string(&self.last_ping_timestamp).as_str())?; + } + if self.timestamp != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("timestamp", ToString::to_string(&self.timestamp).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Pong { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "last_ping_timestamp", + "lastPingTimestamp", + "timestamp", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + LastPingTimestamp, + Timestamp, + __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 { + "lastPingTimestamp" | "last_ping_timestamp" => Ok(GeneratedField::LastPingTimestamp), + "timestamp" => Ok(GeneratedField::Timestamp), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Pong; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.Pong") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut last_ping_timestamp__ = None; + let mut timestamp__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::LastPingTimestamp => { + if last_ping_timestamp__.is_some() { + return Err(serde::de::Error::duplicate_field("lastPingTimestamp")); + } + last_ping_timestamp__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Timestamp => { + if timestamp__.is_some() { + return Err(serde::de::Error::duplicate_field("timestamp")); + } + timestamp__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(Pong { + last_ping_timestamp: last_ping_timestamp__.unwrap_or_default(), + timestamp: timestamp__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.Pong", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ProviderInfo { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.id.is_empty() { + len += 1; + } + if !self.name.is_empty() { + len += 1; + } + if self.r#type != 0 { + len += 1; + } + if self.prevent_transfer { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.ProviderInfo", len)?; + if !self.id.is_empty() { + struct_ser.serialize_field("id", &self.id)?; + } + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + if self.r#type != 0 { + let v = ProviderType::try_from(self.r#type) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.r#type)))?; + struct_ser.serialize_field("type", &v)?; + } + if self.prevent_transfer { + struct_ser.serialize_field("preventTransfer", &self.prevent_transfer)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ProviderInfo { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "id", + "name", + "type", + "prevent_transfer", + "preventTransfer", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Id, + Name, + Type, + PreventTransfer, + __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 { + "id" => Ok(GeneratedField::Id), + "name" => Ok(GeneratedField::Name), + "type" => Ok(GeneratedField::Type), + "preventTransfer" | "prevent_transfer" => Ok(GeneratedField::PreventTransfer), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ProviderInfo; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.ProviderInfo") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut id__ = None; + let mut name__ = None; + let mut r#type__ = None; + let mut prevent_transfer__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id__ = Some(map_.next_value()?); + } + GeneratedField::Name => { + if name__.is_some() { + return Err(serde::de::Error::duplicate_field("name")); + } + name__ = Some(map_.next_value()?); + } + GeneratedField::Type => { + if r#type__.is_some() { + return Err(serde::de::Error::duplicate_field("type")); + } + r#type__ = Some(map_.next_value::()? as i32); + } + GeneratedField::PreventTransfer => { + if prevent_transfer__.is_some() { + return Err(serde::de::Error::duplicate_field("preventTransfer")); + } + prevent_transfer__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(ProviderInfo { + id: id__.unwrap_or_default(), + name: name__.unwrap_or_default(), + r#type: r#type__.unwrap_or_default(), + prevent_transfer: prevent_transfer__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.ProviderInfo", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ProviderType { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Unknown => "PROVIDER_TYPE_UNKNOWN", + Self::Internal => "PROVIDER_TYPE_INTERNAL", + Self::External => "PROVIDER_TYPE_EXTERNAL", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for ProviderType { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "PROVIDER_TYPE_UNKNOWN", + "PROVIDER_TYPE_INTERNAL", + "PROVIDER_TYPE_EXTERNAL", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ProviderType; + + 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 { + "PROVIDER_TYPE_UNKNOWN" => Ok(ProviderType::Unknown), + "PROVIDER_TYPE_INTERNAL" => Ok(ProviderType::Internal), + "PROVIDER_TYPE_EXTERNAL" => Ok(ProviderType::External), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for ProxyConfig { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.url.is_empty() { + len += 1; + } + if !self.username.is_empty() { + len += 1; + } + if !self.password.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.ProxyConfig", len)?; + if !self.url.is_empty() { + struct_ser.serialize_field("url", &self.url)?; + } + if !self.username.is_empty() { + struct_ser.serialize_field("username", &self.username)?; + } + if !self.password.is_empty() { + struct_ser.serialize_field("password", &self.password)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ProxyConfig { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "url", + "username", + "password", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Url, + Username, + Password, + __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 { + "url" => Ok(GeneratedField::Url), + "username" => Ok(GeneratedField::Username), + "password" => Ok(GeneratedField::Password), _ => Ok(GeneratedField::__SkipField__), } } @@ -21333,59 +21695,55 @@ impl<'de> serde::Deserialize<'de> for PlayoutDelay { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = PlayoutDelay; + type Value = ProxyConfig; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.PlayoutDelay") + formatter.write_str("struct livekit.ProxyConfig") } - 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 enabled__ = None; - let mut min__ = None; - let mut max__ = None; + let mut url__ = None; + let mut username__ = None; + let mut password__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Enabled => { - if enabled__.is_some() { - return Err(serde::de::Error::duplicate_field("enabled")); + GeneratedField::Url => { + if url__.is_some() { + return Err(serde::de::Error::duplicate_field("url")); } - enabled__ = Some(map_.next_value()?); + url__ = Some(map_.next_value()?); } - GeneratedField::Min => { - if min__.is_some() { - return Err(serde::de::Error::duplicate_field("min")); + GeneratedField::Username => { + if username__.is_some() { + return Err(serde::de::Error::duplicate_field("username")); } - min__ = - Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) - ; + username__ = Some(map_.next_value()?); } - GeneratedField::Max => { - if max__.is_some() { - return Err(serde::de::Error::duplicate_field("max")); + GeneratedField::Password => { + if password__.is_some() { + return Err(serde::de::Error::duplicate_field("password")); } - max__ = - Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) - ; + password__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(PlayoutDelay { - enabled: enabled__.unwrap_or_default(), - min: min__.unwrap_or_default(), - max: max__.unwrap_or_default(), + Ok(ProxyConfig { + url: url__.unwrap_or_default(), + username: username__.unwrap_or_default(), + password: password__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.PlayoutDelay", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.ProxyConfig", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for Pong { +impl serde::Serialize for PublishDataTrackRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -21393,42 +21751,56 @@ impl serde::Serialize for Pong { { use serde::ser::SerializeStruct; let mut len = 0; - if self.last_ping_timestamp != 0 { + if self.handle != 0 { len += 1; } - if self.timestamp != 0 { + if !self.name.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.Pong", len)?; - if self.last_ping_timestamp != 0 { - #[allow(clippy::needless_borrow)] - #[allow(clippy::needless_borrows_for_generic_args)] - struct_ser.serialize_field("lastPingTimestamp", ToString::to_string(&self.last_ping_timestamp).as_str())?; + if !self.mime_type.is_empty() { + len += 1; } - if self.timestamp != 0 { - #[allow(clippy::needless_borrow)] - #[allow(clippy::needless_borrows_for_generic_args)] - struct_ser.serialize_field("timestamp", ToString::to_string(&self.timestamp).as_str())?; + if self.encryption != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.PublishDataTrackRequest", len)?; + if self.handle != 0 { + struct_ser.serialize_field("handle", &self.handle)?; + } + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + if !self.mime_type.is_empty() { + struct_ser.serialize_field("mimeType", &self.mime_type)?; + } + if self.encryption != 0 { + let v = encryption::Type::try_from(self.encryption) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; + struct_ser.serialize_field("encryption", &v)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for Pong { +impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "last_ping_timestamp", - "lastPingTimestamp", - "timestamp", + "handle", + "name", + "mime_type", + "mimeType", + "encryption", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - LastPingTimestamp, - Timestamp, + Handle, + Name, + MimeType, + Encryption, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -21451,8 +21823,10 @@ impl<'de> serde::Deserialize<'de> for Pong { E: serde::de::Error, { match value { - "lastPingTimestamp" | "last_ping_timestamp" => Ok(GeneratedField::LastPingTimestamp), - "timestamp" => Ok(GeneratedField::Timestamp), + "handle" => Ok(GeneratedField::Handle), + "name" => Ok(GeneratedField::Name), + "mimeType" | "mime_type" => Ok(GeneratedField::MimeType), + "encryption" => Ok(GeneratedField::Encryption), _ => Ok(GeneratedField::__SkipField__), } } @@ -21462,51 +21836,65 @@ impl<'de> serde::Deserialize<'de> for Pong { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = Pong; + type Value = PublishDataTrackRequest; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.Pong") + formatter.write_str("struct livekit.PublishDataTrackRequest") } - 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 last_ping_timestamp__ = None; - let mut timestamp__ = None; + let mut handle__ = None; + let mut name__ = None; + let mut mime_type__ = None; + let mut encryption__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::LastPingTimestamp => { - if last_ping_timestamp__.is_some() { - return Err(serde::de::Error::duplicate_field("lastPingTimestamp")); + GeneratedField::Handle => { + if handle__.is_some() { + return Err(serde::de::Error::duplicate_field("handle")); } - last_ping_timestamp__ = + handle__ = Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) ; } - GeneratedField::Timestamp => { - if timestamp__.is_some() { - return Err(serde::de::Error::duplicate_field("timestamp")); + GeneratedField::Name => { + if name__.is_some() { + return Err(serde::de::Error::duplicate_field("name")); } - timestamp__ = - Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) - ; + name__ = Some(map_.next_value()?); + } + GeneratedField::MimeType => { + if mime_type__.is_some() { + return Err(serde::de::Error::duplicate_field("mimeType")); + } + mime_type__ = Some(map_.next_value()?); + } + GeneratedField::Encryption => { + if encryption__.is_some() { + return Err(serde::de::Error::duplicate_field("encryption")); + } + encryption__ = Some(map_.next_value::()? as i32); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(Pong { - last_ping_timestamp: last_ping_timestamp__.unwrap_or_default(), - timestamp: timestamp__.unwrap_or_default(), + Ok(PublishDataTrackRequest { + handle: handle__.unwrap_or_default(), + name: name__.unwrap_or_default(), + mime_type: mime_type__.unwrap_or_default(), + encryption: encryption__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.Pong", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.PublishDataTrackRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for ProxyConfig { +impl serde::Serialize for PublishDataTrackResponse { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -21514,45 +21902,37 @@ impl serde::Serialize for ProxyConfig { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.url.is_empty() { - len += 1; - } - if !self.username.is_empty() { + if self.handle != 0 { len += 1; } - if !self.password.is_empty() { + if self.info.is_some() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.ProxyConfig", len)?; - if !self.url.is_empty() { - struct_ser.serialize_field("url", &self.url)?; - } - if !self.username.is_empty() { - struct_ser.serialize_field("username", &self.username)?; + let mut struct_ser = serializer.serialize_struct("livekit.PublishDataTrackResponse", len)?; + if self.handle != 0 { + struct_ser.serialize_field("handle", &self.handle)?; } - if !self.password.is_empty() { - struct_ser.serialize_field("password", &self.password)?; + if let Some(v) = self.info.as_ref() { + struct_ser.serialize_field("info", v)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for ProxyConfig { +impl<'de> serde::Deserialize<'de> for PublishDataTrackResponse { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "url", - "username", - "password", + "handle", + "info", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Url, - Username, - Password, + Handle, + Info, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -21575,9 +21955,8 @@ impl<'de> serde::Deserialize<'de> for ProxyConfig { E: serde::de::Error, { match value { - "url" => Ok(GeneratedField::Url), - "username" => Ok(GeneratedField::Username), - "password" => Ok(GeneratedField::Password), + "handle" => Ok(GeneratedField::Handle), + "info" => Ok(GeneratedField::Info), _ => Ok(GeneratedField::__SkipField__), } } @@ -21587,52 +21966,46 @@ impl<'de> serde::Deserialize<'de> for ProxyConfig { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = ProxyConfig; + type Value = PublishDataTrackResponse; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.ProxyConfig") + formatter.write_str("struct livekit.PublishDataTrackResponse") } - 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 url__ = None; - let mut username__ = None; - let mut password__ = None; + let mut handle__ = None; + let mut info__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Url => { - if url__.is_some() { - return Err(serde::de::Error::duplicate_field("url")); - } - url__ = Some(map_.next_value()?); - } - GeneratedField::Username => { - if username__.is_some() { - return Err(serde::de::Error::duplicate_field("username")); + GeneratedField::Handle => { + if handle__.is_some() { + return Err(serde::de::Error::duplicate_field("handle")); } - username__ = Some(map_.next_value()?); + handle__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; } - GeneratedField::Password => { - if password__.is_some() { - return Err(serde::de::Error::duplicate_field("password")); + GeneratedField::Info => { + if info__.is_some() { + return Err(serde::de::Error::duplicate_field("info")); } - password__ = Some(map_.next_value()?); + info__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(ProxyConfig { - url: url__.unwrap_or_default(), - username: username__.unwrap_or_default(), - password: password__.unwrap_or_default(), + Ok(PublishDataTrackResponse { + handle: handle__.unwrap_or_default(), + info: info__, }) } } - deserializer.deserialize_struct("livekit.ProxyConfig", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.PublishDataTrackResponse", FIELDS, GeneratedVisitor) } } impl serde::Serialize for RtcpSenderReportState { @@ -24411,6 +24784,12 @@ impl serde::Serialize for RequestResponse { request_response::Request::UpdateVideoTrack(v) => { struct_ser.serialize_field("updateVideoTrack", v)?; } + request_response::Request::PublishDataTrack(v) => { + struct_ser.serialize_field("publishDataTrack", v)?; + } + request_response::Request::UnpublishDataTrack(v) => { + struct_ser.serialize_field("unpublishDataTrack", v)?; + } } } struct_ser.end() @@ -24437,6 +24816,10 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "updateAudioTrack", "update_video_track", "updateVideoTrack", + "publish_data_track", + "publishDataTrack", + "unpublish_data_track", + "unpublishDataTrack", ]; #[allow(clippy::enum_variant_names)] @@ -24450,6 +24833,8 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { UpdateMetadata, UpdateAudioTrack, UpdateVideoTrack, + PublishDataTrack, + UnpublishDataTrack, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -24481,6 +24866,8 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "updateMetadata" | "update_metadata" => Ok(GeneratedField::UpdateMetadata), "updateAudioTrack" | "update_audio_track" => Ok(GeneratedField::UpdateAudioTrack), "updateVideoTrack" | "update_video_track" => Ok(GeneratedField::UpdateVideoTrack), + "publishDataTrack" | "publish_data_track" => Ok(GeneratedField::PublishDataTrack), + "unpublishDataTrack" | "unpublish_data_track" => Ok(GeneratedField::UnpublishDataTrack), _ => Ok(GeneratedField::__SkipField__), } } @@ -24566,6 +24953,20 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { return Err(serde::de::Error::duplicate_field("updateVideoTrack")); } request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UpdateVideoTrack) +; + } + GeneratedField::PublishDataTrack => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("publishDataTrack")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::PublishDataTrack) +; + } + GeneratedField::UnpublishDataTrack => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("unpublishDataTrack")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UnpublishDataTrack) ; } GeneratedField::__SkipField__ => { @@ -24598,6 +24999,10 @@ impl serde::Serialize for request_response::Reason { Self::Queued => "QUEUED", Self::UnsupportedType => "UNSUPPORTED_TYPE", Self::UnclassifiedError => "UNCLASSIFIED_ERROR", + Self::DataTrackInvalidHandle => "DATA_TRACK_INVALID_HANDLE", + Self::DataTrackInvalidName => "DATA_TRACK_INVALID_NAME", + Self::DataTrackInvalidMimeType => "DATA_TRACK_INVALID_MIME_TYPE", + Self::DataTrackNameTaken => "DATA_TRACK_NAME_TAKEN", }; serializer.serialize_str(variant) } @@ -24616,6 +25021,10 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "QUEUED", "UNSUPPORTED_TYPE", "UNCLASSIFIED_ERROR", + "DATA_TRACK_INVALID_HANDLE", + "DATA_TRACK_INVALID_NAME", + "DATA_TRACK_INVALID_MIME_TYPE", + "DATA_TRACK_NAME_TAKEN", ]; struct GeneratedVisitor; @@ -24663,6 +25072,10 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "QUEUED" => Ok(request_response::Reason::Queued), "UNSUPPORTED_TYPE" => Ok(request_response::Reason::UnsupportedType), "UNCLASSIFIED_ERROR" => Ok(request_response::Reason::UnclassifiedError), + "DATA_TRACK_INVALID_HANDLE" => Ok(request_response::Reason::DataTrackInvalidHandle), + "DATA_TRACK_INVALID_NAME" => Ok(request_response::Reason::DataTrackInvalidName), + "DATA_TRACK_INVALID_MIME_TYPE" => Ok(request_response::Reason::DataTrackInvalidMimeType), + "DATA_TRACK_NAME_TAKEN" => Ok(request_response::Reason::DataTrackNameTaken), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -27321,6 +27734,9 @@ impl serde::Serialize for SipCallInfo { if !self.call_context.is_empty() { len += 1; } + if self.provider_info.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.SIPCallInfo", len)?; if !self.call_id.is_empty() { struct_ser.serialize_field("callId", &self.call_id)?; @@ -27422,6 +27838,9 @@ impl serde::Serialize for SipCallInfo { if !self.call_context.is_empty() { struct_ser.serialize_field("callContext", &self.call_context)?; } + if let Some(v) = self.provider_info.as_ref() { + struct_ser.serialize_field("providerInfo", v)?; + } struct_ser.end() } } @@ -27482,6 +27901,8 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { "pcapFileLink", "call_context", "callContext", + "provider_info", + "providerInfo", ]; #[allow(clippy::enum_variant_names)] @@ -27512,6 +27933,7 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { MediaEncryption, PcapFileLink, CallContext, + ProviderInfo, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -27560,6 +27982,7 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { "mediaEncryption" | "media_encryption" => Ok(GeneratedField::MediaEncryption), "pcapFileLink" | "pcap_file_link" => Ok(GeneratedField::PcapFileLink), "callContext" | "call_context" => Ok(GeneratedField::CallContext), + "providerInfo" | "provider_info" => Ok(GeneratedField::ProviderInfo), _ => Ok(GeneratedField::__SkipField__), } } @@ -27605,6 +28028,7 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { let mut media_encryption__ = None; let mut pcap_file_link__ = None; let mut call_context__ = None; + let mut provider_info__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::CallId => { @@ -27777,6 +28201,12 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { } call_context__ = Some(map_.next_value()?); } + GeneratedField::ProviderInfo => { + if provider_info__.is_some() { + return Err(serde::de::Error::duplicate_field("providerInfo")); + } + provider_info__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -27809,6 +28239,7 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { media_encryption: media_encryption__.unwrap_or_default(), pcap_file_link: pcap_file_link__.unwrap_or_default(), call_context: call_context__.unwrap_or_default(), + provider_info: provider_info__, }) } } @@ -33281,6 +33712,9 @@ impl serde::Serialize for SessionDescription { if self.id != 0 { len += 1; } + if !self.mid_to_track_id.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.SessionDescription", len)?; if !self.r#type.is_empty() { struct_ser.serialize_field("type", &self.r#type)?; @@ -33291,6 +33725,9 @@ impl serde::Serialize for SessionDescription { if self.id != 0 { struct_ser.serialize_field("id", &self.id)?; } + if !self.mid_to_track_id.is_empty() { + struct_ser.serialize_field("midToTrackId", &self.mid_to_track_id)?; + } struct_ser.end() } } @@ -33304,6 +33741,8 @@ impl<'de> serde::Deserialize<'de> for SessionDescription { "type", "sdp", "id", + "mid_to_track_id", + "midToTrackId", ]; #[allow(clippy::enum_variant_names)] @@ -33311,6 +33750,7 @@ impl<'de> serde::Deserialize<'de> for SessionDescription { Type, Sdp, Id, + MidToTrackId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -33336,6 +33776,7 @@ impl<'de> serde::Deserialize<'de> for SessionDescription { "type" => Ok(GeneratedField::Type), "sdp" => Ok(GeneratedField::Sdp), "id" => Ok(GeneratedField::Id), + "midToTrackId" | "mid_to_track_id" => Ok(GeneratedField::MidToTrackId), _ => Ok(GeneratedField::__SkipField__), } } @@ -33358,6 +33799,7 @@ impl<'de> serde::Deserialize<'de> for SessionDescription { let mut r#type__ = None; let mut sdp__ = None; let mut id__ = None; + let mut mid_to_track_id__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Type => { @@ -33380,6 +33822,14 @@ impl<'de> serde::Deserialize<'de> for SessionDescription { Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) ; } + GeneratedField::MidToTrackId => { + if mid_to_track_id__.is_some() { + return Err(serde::de::Error::duplicate_field("midToTrackId")); + } + mid_to_track_id__ = Some( + map_.next_value::>()? + ); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -33389,6 +33839,7 @@ impl<'de> serde::Deserialize<'de> for SessionDescription { r#type: r#type__.unwrap_or_default(), sdp: sdp__.unwrap_or_default(), id: id__.unwrap_or_default(), + mid_to_track_id: mid_to_track_id__.unwrap_or_default(), }) } } @@ -33462,8 +33913,11 @@ impl serde::Serialize for SignalRequest { signal_request::Message::UpdateVideoTrack(v) => { struct_ser.serialize_field("updateVideoTrack", v)?; } - signal_request::Message::AddDataTrack(v) => { - struct_ser.serialize_field("addDataTrack", v)?; + signal_request::Message::PublishDataTrack(v) => { + struct_ser.serialize_field("publishDataTrack", v)?; + } + signal_request::Message::UnpublishDataTrack(v) => { + struct_ser.serialize_field("unpublishDataTrack", v)?; } signal_request::Message::UpdateDataSubscription(v) => { struct_ser.serialize_field("updateDataSubscription", v)?; @@ -33506,8 +33960,10 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "updateAudioTrack", "update_video_track", "updateVideoTrack", - "add_data_track", - "addDataTrack", + "publish_data_track", + "publishDataTrack", + "unpublish_data_track", + "unpublishDataTrack", "update_data_subscription", "updateDataSubscription", ]; @@ -33531,7 +33987,8 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { PingReq, UpdateAudioTrack, UpdateVideoTrack, - AddDataTrack, + PublishDataTrack, + UnpublishDataTrack, UpdateDataSubscription, __SkipField__, } @@ -33572,7 +34029,8 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "pingReq" | "ping_req" => Ok(GeneratedField::PingReq), "updateAudioTrack" | "update_audio_track" => Ok(GeneratedField::UpdateAudioTrack), "updateVideoTrack" | "update_video_track" => Ok(GeneratedField::UpdateVideoTrack), - "addDataTrack" | "add_data_track" => Ok(GeneratedField::AddDataTrack), + "publishDataTrack" | "publish_data_track" => Ok(GeneratedField::PublishDataTrack), + "unpublishDataTrack" | "unpublish_data_track" => Ok(GeneratedField::UnpublishDataTrack), "updateDataSubscription" | "update_data_subscription" => Ok(GeneratedField::UpdateDataSubscription), _ => Ok(GeneratedField::__SkipField__), } @@ -33714,11 +34172,18 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UpdateVideoTrack) ; } - GeneratedField::AddDataTrack => { + GeneratedField::PublishDataTrack => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("publishDataTrack")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::PublishDataTrack) +; + } + GeneratedField::UnpublishDataTrack => { if message__.is_some() { - return Err(serde::de::Error::duplicate_field("addDataTrack")); + return Err(serde::de::Error::duplicate_field("unpublishDataTrack")); } - message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::AddDataTrack) + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UnpublishDataTrack) ; } GeneratedField::UpdateDataSubscription => { @@ -33832,8 +34297,14 @@ impl serde::Serialize for SignalResponse { signal_response::Message::SubscribedAudioCodecUpdate(v) => { struct_ser.serialize_field("subscribedAudioCodecUpdate", v)?; } - signal_response::Message::DataTrackPublished(v) => { - struct_ser.serialize_field("dataTrackPublished", v)?; + signal_response::Message::PublishDataTrack(v) => { + struct_ser.serialize_field("publishDataTrack", v)?; + } + signal_response::Message::UnpublishDataTrack(v) => { + struct_ser.serialize_field("unpublishDataTrack", v)?; + } + signal_response::Message::DataTrackHandles(v) => { + struct_ser.serialize_field("dataTrackHandles", v)?; } } } @@ -33888,8 +34359,12 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "mediaSectionsRequirement", "subscribed_audio_codec_update", "subscribedAudioCodecUpdate", - "data_track_published", - "dataTrackPublished", + "publish_data_track", + "publishDataTrack", + "unpublish_data_track", + "unpublishDataTrack", + "data_track_handles", + "dataTrackHandles", ]; #[allow(clippy::enum_variant_names)] @@ -33919,7 +34394,9 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { RoomMoved, MediaSectionsRequirement, SubscribedAudioCodecUpdate, - DataTrackPublished, + PublishDataTrack, + UnpublishDataTrack, + DataTrackHandles, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -33967,7 +34444,9 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "roomMoved" | "room_moved" => Ok(GeneratedField::RoomMoved), "mediaSectionsRequirement" | "media_sections_requirement" => Ok(GeneratedField::MediaSectionsRequirement), "subscribedAudioCodecUpdate" | "subscribed_audio_codec_update" => Ok(GeneratedField::SubscribedAudioCodecUpdate), - "dataTrackPublished" | "data_track_published" => Ok(GeneratedField::DataTrackPublished), + "publishDataTrack" | "publish_data_track" => Ok(GeneratedField::PublishDataTrack), + "unpublishDataTrack" | "unpublish_data_track" => Ok(GeneratedField::UnpublishDataTrack), + "dataTrackHandles" | "data_track_handles" => Ok(GeneratedField::DataTrackHandles), _ => Ok(GeneratedField::__SkipField__), } } @@ -34163,11 +34642,25 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::SubscribedAudioCodecUpdate) ; } - GeneratedField::DataTrackPublished => { + GeneratedField::PublishDataTrack => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("publishDataTrack")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::PublishDataTrack) +; + } + GeneratedField::UnpublishDataTrack => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("unpublishDataTrack")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::UnpublishDataTrack) +; + } + GeneratedField::DataTrackHandles => { if message__.is_some() { - return Err(serde::de::Error::duplicate_field("dataTrackPublished")); + return Err(serde::de::Error::duplicate_field("dataTrackHandles")); } - message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::DataTrackPublished) + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::DataTrackHandles) ; } GeneratedField::__SkipField__ => { @@ -39978,6 +40471,200 @@ impl<'de> serde::Deserialize<'de> for TrickleRequest { deserializer.deserialize_struct("livekit.TrickleRequest", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for UnpublishDataTrackRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.handle != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.UnpublishDataTrackRequest", len)?; + if self.handle != 0 { + struct_ser.serialize_field("handle", &self.handle)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for UnpublishDataTrackRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "handle", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Handle, + __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 { + "handle" => Ok(GeneratedField::Handle), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = UnpublishDataTrackRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.UnpublishDataTrackRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut handle__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Handle => { + if handle__.is_some() { + return Err(serde::de::Error::duplicate_field("handle")); + } + handle__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(UnpublishDataTrackRequest { + handle: handle__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.UnpublishDataTrackRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for UnpublishDataTrackResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.handle != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.UnpublishDataTrackResponse", len)?; + if self.handle != 0 { + struct_ser.serialize_field("handle", &self.handle)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for UnpublishDataTrackResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "handle", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Handle, + __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 { + "handle" => Ok(GeneratedField::Handle), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = UnpublishDataTrackResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.UnpublishDataTrackResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut handle__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Handle => { + if handle__.is_some() { + return Err(serde::de::Error::duplicate_field("handle")); + } + handle__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(UnpublishDataTrackResponse { + handle: handle__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.UnpublishDataTrackResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for UpdateDataSubscription { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -39986,44 +40673,138 @@ impl serde::Serialize for UpdateDataSubscription { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.track_sids.is_empty() { + if !self.updates.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.UpdateDataSubscription", len)?; + if !self.updates.is_empty() { + struct_ser.serialize_field("updates", &self.updates)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for UpdateDataSubscription { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "updates", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Updates, + __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 { + "updates" => Ok(GeneratedField::Updates), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = UpdateDataSubscription; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.UpdateDataSubscription") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut updates__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Updates => { + if updates__.is_some() { + return Err(serde::de::Error::duplicate_field("updates")); + } + updates__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(UpdateDataSubscription { + updates: updates__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.UpdateDataSubscription", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for update_data_subscription::Update { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.sid.is_empty() { len += 1; } if self.subscribe { len += 1; } - if !self.options.is_empty() { + if self.options.is_some() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.UpdateDataSubscription", len)?; - if !self.track_sids.is_empty() { - struct_ser.serialize_field("trackSids", &self.track_sids)?; + let mut struct_ser = serializer.serialize_struct("livekit.UpdateDataSubscription.Update", len)?; + if !self.sid.is_empty() { + struct_ser.serialize_field("sid", &self.sid)?; } if self.subscribe { struct_ser.serialize_field("subscribe", &self.subscribe)?; } - if !self.options.is_empty() { - struct_ser.serialize_field("options", &self.options)?; + if let Some(v) = self.options.as_ref() { + struct_ser.serialize_field("options", v)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for UpdateDataSubscription { +impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "track_sids", - "trackSids", + "sid", "subscribe", "options", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - TrackSids, + Sid, Subscribe, Options, __SkipField__, @@ -40048,7 +40829,7 @@ impl<'de> serde::Deserialize<'de> for UpdateDataSubscription { E: serde::de::Error, { match value { - "trackSids" | "track_sids" => Ok(GeneratedField::TrackSids), + "sid" => Ok(GeneratedField::Sid), "subscribe" => Ok(GeneratedField::Subscribe), "options" => Ok(GeneratedField::Options), _ => Ok(GeneratedField::__SkipField__), @@ -40060,26 +40841,26 @@ impl<'de> serde::Deserialize<'de> for UpdateDataSubscription { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = UpdateDataSubscription; + type Value = update_data_subscription::Update; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.UpdateDataSubscription") + formatter.write_str("struct livekit.UpdateDataSubscription.Update") } - 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 track_sids__ = None; + let mut sid__ = None; let mut subscribe__ = None; let mut options__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::TrackSids => { - if track_sids__.is_some() { - return Err(serde::de::Error::duplicate_field("trackSids")); + GeneratedField::Sid => { + if sid__.is_some() { + return Err(serde::de::Error::duplicate_field("sid")); } - track_sids__ = Some(map_.next_value()?); + sid__ = Some(map_.next_value()?); } GeneratedField::Subscribe => { if subscribe__.is_some() { @@ -40091,21 +40872,21 @@ impl<'de> serde::Deserialize<'de> for UpdateDataSubscription { if options__.is_some() { return Err(serde::de::Error::duplicate_field("options")); } - options__ = Some(map_.next_value()?); + options__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(UpdateDataSubscription { - track_sids: track_sids__.unwrap_or_default(), + Ok(update_data_subscription::Update { + sid: sid__.unwrap_or_default(), subscribe: subscribe__.unwrap_or_default(), - options: options__.unwrap_or_default(), + options: options__, }) } } - deserializer.deserialize_struct("livekit.UpdateDataSubscription", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.UpdateDataSubscription.Update", FIELDS, GeneratedVisitor) } } impl serde::Serialize for UpdateIngressRequest { From 483dff352ba8a68ec8f363ea58f2e0e9cbd2f1c3 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:49:53 +1100 Subject: [PATCH 008/232] Expose DTP types --- livekit-datatrack/src/dtp/deserialize.rs | 2 +- livekit-datatrack/src/dtp/{lib.rs => mod.rs} | 7 ++++++- livekit-datatrack/src/dtp/packet.rs | 14 +++++++++++++- livekit-datatrack/src/dtp/serialize.rs | 5 ++--- 4 files changed, 22 insertions(+), 6 deletions(-) rename livekit-datatrack/src/dtp/{lib.rs => mod.rs} (85%) diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs index e662e777e..c19107b61 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{ +use super::{ packet::{Dtp, E2ee, Header, consts::*}, track_handle::{TrackHandle, TrackHandleError}, }; diff --git a/livekit-datatrack/src/dtp/lib.rs b/livekit-datatrack/src/dtp/mod.rs similarity index 85% rename from livekit-datatrack/src/dtp/lib.rs rename to livekit-datatrack/src/dtp/mod.rs index fd82f7537..3fc75bc32 100644 --- a/livekit-datatrack/src/dtp/lib.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -15,4 +15,9 @@ mod packet; mod track_handle; mod deserialize; -mod serialize; \ No newline at end of file +mod serialize; + +pub use packet::*; +pub use track_handle::*; +pub use deserialize::*; +pub use serialize::*; \ No newline at end of file diff --git a/livekit-datatrack/src/dtp/packet.rs b/livekit-datatrack/src/dtp/packet.rs index 468e0a3b5..073605bf2 100644 --- a/livekit-datatrack/src/dtp/packet.rs +++ b/livekit-datatrack/src/dtp/packet.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::track_handle::TrackHandle; +use super::track_handle::TrackHandle; use bytes::Bytes; use core::fmt; @@ -40,6 +40,18 @@ pub struct E2ee { pub iv: [u8; 12], } +impl Dtp { + /// Whether the packet is the final one in a frame. + pub fn is_final(&self) -> bool { + self.header.is_final + } + + /// Whether the packet's payload is encrypted. + pub fn is_encrypted(&self) -> bool { + self.header.e2ee.is_some() + } +} + impl fmt::Debug for Dtp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Dtp") diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/dtp/serialize.rs index cdd5c22cf..416fc277d 100644 --- a/livekit-datatrack/src/dtp/serialize.rs +++ b/livekit-datatrack/src/dtp/serialize.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::packet::{Dtp, Header, consts::*}; +use super::packet::{Dtp, Header, consts::*}; use bytes::{BufMut, Bytes, BytesMut}; use thiserror::Error; @@ -138,8 +138,7 @@ impl Header { #[cfg(test)] mod tests { - use super::*; - use crate::packet::E2ee; + use super::{*, super::packet::E2ee}; use bytes::Buf; /// Constructed packet to use in tests. From d28aa19d1c3e8df75eb038a27327b766c0e3eadb Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:33:34 +1100 Subject: [PATCH 009/232] Update protocol --- livekit-protocol/protocol | 2 +- livekit-protocol/src/livekit.rs | 73 ++++----- livekit-protocol/src/livekit.serde.rs | 221 ++++++++++++-------------- 3 files changed, 140 insertions(+), 156 deletions(-) diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index 2a0a69c0d..5386ca2ff 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit 2a0a69c0d7a863a888f0da003d79558db7411358 +Subproject commit 5386ca2ff8bad8f187e2d0e9037e82150e3ae0e4 diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index 992dcb0b6..3475b9af3 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -590,15 +590,15 @@ pub struct TrackInfo { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DataTrackInfo { - #[prost(string, tag="1")] + /// Client-assigned, 16-bit identifier that will be attached to packets sent by the publisher. + #[prost(uint32, tag="1")] + pub pub_handle: u32, + /// Server-assigned track identifier. + #[prost(string, tag="2")] pub sid: ::prost::alloc::string::String, /// Human-readable identifier (e.g., `geoLocation`, `servoPosition.x`, etc.), unique per publisher. - #[prost(string, tag="2")] - pub name: ::prost::alloc::string::String, - /// MIME type of the data sent over the track (e.g., `application/json`). - /// This must be a valid MIME type as defined by RFC 2046. #[prost(string, tag="3")] - pub mime_type: ::prost::alloc::string::String, + pub name: ::prost::alloc::string::String, /// Method used for end-to-end encryption (E2EE) on packet payloads. #[prost(enumeration="encryption::Type", tag="4")] pub encryption: i32, @@ -3294,50 +3294,45 @@ pub struct AddTrackRequest { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PublishDataTrackRequest { - /// 16-bit identifier attached to packets, unique per publisher. + /// Client-assigned, 16-bit identifier that will be attached to packets sent by the publisher. + /// This must be non-zero and unique for each data track published by the publisher. #[prost(uint32, tag="1")] - pub handle: u32, + pub pub_handle: u32, /// Human-readable identifier (e.g., `geoLocation`, `servoPosition.x`, etc.), unique per publisher. + /// This must be non-empty and no longer than 256 characters. #[prost(string, tag="2")] pub name: ::prost::alloc::string::String, - /// MIME type of the data sent over the track (e.g., `application/json`). - /// This must be a valid MIME type as defined by RFC 2046. - #[prost(string, tag="3")] - pub mime_type: ::prost::alloc::string::String, /// Method used for end-to-end encryption (E2EE) on frame payloads. - #[prost(enumeration="encryption::Type", tag="4")] + #[prost(enumeration="encryption::Type", tag="3")] pub encryption: i32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PublishDataTrackResponse { - /// Handle of the track that was published. - #[prost(uint32, tag="1")] - pub handle: u32, /// Information about the published track. - #[prost(message, optional, tag="2")] + #[prost(message, optional, tag="1")] pub info: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct UnpublishDataTrackRequest { - /// Handle of the track to unpublish. + /// Publisher handle of the track to unpublish. #[prost(uint32, tag="1")] - pub handle: u32, + pub pub_handle: u32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct UnpublishDataTrackResponse { - /// Handle of the track that was unpublished. + /// Publisher handle of the track that was unpublished. #[prost(uint32, tag="1")] - pub handle: u32, + pub pub_handle: u32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DataTrackSubscriberHandles { - /// Mapping from data track SIDs to handles subscribers will see on incoming packets. - #[prost(map="string, uint32", tag="1")] - pub handles: ::std::collections::HashMap<::prost::alloc::string::String, u32>, + /// Maps handles from incoming packets to the track SIDs that the packets belong to. + #[prost(map="uint32, string", tag="1")] + pub sub_handles: ::std::collections::HashMap, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3467,11 +3462,13 @@ pub mod update_data_subscription { pub struct Update { #[prost(string, tag="1")] pub sid: ::prost::alloc::string::String, - #[prost(bool, tag="2")] + #[prost(string, tag="2")] + pub participant_identity: ::prost::alloc::string::String, + #[prost(bool, tag="3")] pub subscribe: bool, /// Options to apply when initially subscribing or updating an existing subscription. /// When unsubscribing, this field is ignored. - #[prost(message, optional, tag="3")] + #[prost(message, optional, tag="4")] pub options: ::core::option::Option, } } @@ -3879,10 +3876,10 @@ pub mod request_response { Queued = 4, UnsupportedType = 5, UnclassifiedError = 6, - DataTrackInvalidHandle = 7, - DataTrackInvalidName = 8, - DataTrackInvalidMimeType = 9, - DataTrackNameTaken = 10, + InvalidHandle = 7, + InvalidName = 8, + DuplicateHandle = 9, + DuplicateName = 10, } impl Reason { /// String value of the enum field names used in the ProtoBuf definition. @@ -3898,10 +3895,10 @@ pub mod request_response { Reason::Queued => "QUEUED", Reason::UnsupportedType => "UNSUPPORTED_TYPE", Reason::UnclassifiedError => "UNCLASSIFIED_ERROR", - Reason::DataTrackInvalidHandle => "DATA_TRACK_INVALID_HANDLE", - Reason::DataTrackInvalidName => "DATA_TRACK_INVALID_NAME", - Reason::DataTrackInvalidMimeType => "DATA_TRACK_INVALID_MIME_TYPE", - Reason::DataTrackNameTaken => "DATA_TRACK_NAME_TAKEN", + Reason::InvalidHandle => "INVALID_HANDLE", + Reason::InvalidName => "INVALID_NAME", + Reason::DuplicateHandle => "DUPLICATE_HANDLE", + Reason::DuplicateName => "DUPLICATE_NAME", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -3914,10 +3911,10 @@ pub mod request_response { "QUEUED" => Some(Self::Queued), "UNSUPPORTED_TYPE" => Some(Self::UnsupportedType), "UNCLASSIFIED_ERROR" => Some(Self::UnclassifiedError), - "DATA_TRACK_INVALID_HANDLE" => Some(Self::DataTrackInvalidHandle), - "DATA_TRACK_INVALID_NAME" => Some(Self::DataTrackInvalidName), - "DATA_TRACK_INVALID_MIME_TYPE" => Some(Self::DataTrackInvalidMimeType), - "DATA_TRACK_NAME_TAKEN" => Some(Self::DataTrackNameTaken), + "INVALID_HANDLE" => Some(Self::InvalidHandle), + "INVALID_NAME" => Some(Self::InvalidName), + "DUPLICATE_HANDLE" => Some(Self::DuplicateHandle), + "DUPLICATE_NAME" => Some(Self::DuplicateName), _ => None, } } diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index 7cd6cfd8c..e025dc1f7 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -7106,28 +7106,28 @@ impl serde::Serialize for DataTrackInfo { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.sid.is_empty() { + if self.pub_handle != 0 { len += 1; } - if !self.name.is_empty() { + if !self.sid.is_empty() { len += 1; } - if !self.mime_type.is_empty() { + if !self.name.is_empty() { len += 1; } if self.encryption != 0 { 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)?; + } if !self.sid.is_empty() { struct_ser.serialize_field("sid", &self.sid)?; } if !self.name.is_empty() { struct_ser.serialize_field("name", &self.name)?; } - if !self.mime_type.is_empty() { - struct_ser.serialize_field("mimeType", &self.mime_type)?; - } if self.encryption != 0 { let v = encryption::Type::try_from(self.encryption) .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; @@ -7143,18 +7143,18 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ + "pub_handle", + "pubHandle", "sid", "name", - "mime_type", - "mimeType", "encryption", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { + PubHandle, Sid, Name, - MimeType, Encryption, __SkipField__, } @@ -7178,9 +7178,9 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { E: serde::de::Error, { match value { + "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), "sid" => Ok(GeneratedField::Sid), "name" => Ok(GeneratedField::Name), - "mimeType" | "mime_type" => Ok(GeneratedField::MimeType), "encryption" => Ok(GeneratedField::Encryption), _ => Ok(GeneratedField::__SkipField__), } @@ -7201,12 +7201,20 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { where V: serde::de::MapAccess<'de>, { + let mut pub_handle__ = None; let mut sid__ = None; let mut name__ = None; - let mut mime_type__ = None; let mut encryption__ = None; while let Some(k) = map_.next_key()? { match k { + GeneratedField::PubHandle => { + if pub_handle__.is_some() { + return Err(serde::de::Error::duplicate_field("pubHandle")); + } + pub_handle__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } GeneratedField::Sid => { if sid__.is_some() { return Err(serde::de::Error::duplicate_field("sid")); @@ -7219,12 +7227,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { } name__ = Some(map_.next_value()?); } - GeneratedField::MimeType => { - if mime_type__.is_some() { - return Err(serde::de::Error::duplicate_field("mimeType")); - } - mime_type__ = Some(map_.next_value()?); - } GeneratedField::Encryption => { if encryption__.is_some() { return Err(serde::de::Error::duplicate_field("encryption")); @@ -7237,9 +7239,9 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { } } Ok(DataTrackInfo { + pub_handle: pub_handle__.unwrap_or_default(), sid: sid__.unwrap_or_default(), name: name__.unwrap_or_default(), - mime_type: mime_type__.unwrap_or_default(), encryption: encryption__.unwrap_or_default(), }) } @@ -7255,12 +7257,12 @@ impl serde::Serialize for DataTrackSubscriberHandles { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.handles.is_empty() { + if !self.sub_handles.is_empty() { len += 1; } let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSubscriberHandles", len)?; - if !self.handles.is_empty() { - struct_ser.serialize_field("handles", &self.handles)?; + if !self.sub_handles.is_empty() { + struct_ser.serialize_field("subHandles", &self.sub_handles)?; } struct_ser.end() } @@ -7272,12 +7274,13 @@ impl<'de> serde::Deserialize<'de> for DataTrackSubscriberHandles { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "handles", + "sub_handles", + "subHandles", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Handles, + SubHandles, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -7300,7 +7303,7 @@ impl<'de> serde::Deserialize<'de> for DataTrackSubscriberHandles { E: serde::de::Error, { match value { - "handles" => Ok(GeneratedField::Handles), + "subHandles" | "sub_handles" => Ok(GeneratedField::SubHandles), _ => Ok(GeneratedField::__SkipField__), } } @@ -7320,16 +7323,16 @@ impl<'de> serde::Deserialize<'de> for DataTrackSubscriberHandles { where V: serde::de::MapAccess<'de>, { - let mut handles__ = None; + let mut sub_handles__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Handles => { - if handles__.is_some() { - return Err(serde::de::Error::duplicate_field("handles")); + GeneratedField::SubHandles => { + if sub_handles__.is_some() { + return Err(serde::de::Error::duplicate_field("subHandles")); } - handles__ = Some( - map_.next_value::>>()? - .into_iter().map(|(k,v)| (k, v.0)).collect() + sub_handles__ = Some( + map_.next_value::, _>>()? + .into_iter().map(|(k,v)| (k.0, v)).collect() ); } GeneratedField::__SkipField__ => { @@ -7338,7 +7341,7 @@ impl<'de> serde::Deserialize<'de> for DataTrackSubscriberHandles { } } Ok(DataTrackSubscriberHandles { - handles: handles__.unwrap_or_default(), + sub_handles: sub_handles__.unwrap_or_default(), }) } } @@ -21751,28 +21754,22 @@ impl serde::Serialize for PublishDataTrackRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if self.handle != 0 { + if self.pub_handle != 0 { len += 1; } if !self.name.is_empty() { len += 1; } - if !self.mime_type.is_empty() { - len += 1; - } if self.encryption != 0 { len += 1; } let mut struct_ser = serializer.serialize_struct("livekit.PublishDataTrackRequest", len)?; - if self.handle != 0 { - struct_ser.serialize_field("handle", &self.handle)?; + if self.pub_handle != 0 { + struct_ser.serialize_field("pubHandle", &self.pub_handle)?; } if !self.name.is_empty() { struct_ser.serialize_field("name", &self.name)?; } - if !self.mime_type.is_empty() { - struct_ser.serialize_field("mimeType", &self.mime_type)?; - } if self.encryption != 0 { let v = encryption::Type::try_from(self.encryption) .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; @@ -21788,18 +21785,16 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "handle", + "pub_handle", + "pubHandle", "name", - "mime_type", - "mimeType", "encryption", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Handle, + PubHandle, Name, - MimeType, Encryption, __SkipField__, } @@ -21823,9 +21818,8 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { E: serde::de::Error, { match value { - "handle" => Ok(GeneratedField::Handle), + "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), "name" => Ok(GeneratedField::Name), - "mimeType" | "mime_type" => Ok(GeneratedField::MimeType), "encryption" => Ok(GeneratedField::Encryption), _ => Ok(GeneratedField::__SkipField__), } @@ -21846,17 +21840,16 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { where V: serde::de::MapAccess<'de>, { - let mut handle__ = None; + let mut pub_handle__ = None; let mut name__ = None; - let mut mime_type__ = None; let mut encryption__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Handle => { - if handle__.is_some() { - return Err(serde::de::Error::duplicate_field("handle")); + GeneratedField::PubHandle => { + if pub_handle__.is_some() { + return Err(serde::de::Error::duplicate_field("pubHandle")); } - handle__ = + pub_handle__ = Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) ; } @@ -21866,12 +21859,6 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { } name__ = Some(map_.next_value()?); } - GeneratedField::MimeType => { - if mime_type__.is_some() { - return Err(serde::de::Error::duplicate_field("mimeType")); - } - mime_type__ = Some(map_.next_value()?); - } GeneratedField::Encryption => { if encryption__.is_some() { return Err(serde::de::Error::duplicate_field("encryption")); @@ -21884,9 +21871,8 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { } } Ok(PublishDataTrackRequest { - handle: handle__.unwrap_or_default(), + pub_handle: pub_handle__.unwrap_or_default(), name: name__.unwrap_or_default(), - mime_type: mime_type__.unwrap_or_default(), encryption: encryption__.unwrap_or_default(), }) } @@ -21902,16 +21888,10 @@ impl serde::Serialize for PublishDataTrackResponse { { use serde::ser::SerializeStruct; let mut len = 0; - if self.handle != 0 { - len += 1; - } if self.info.is_some() { len += 1; } let mut struct_ser = serializer.serialize_struct("livekit.PublishDataTrackResponse", len)?; - if self.handle != 0 { - struct_ser.serialize_field("handle", &self.handle)?; - } if let Some(v) = self.info.as_ref() { struct_ser.serialize_field("info", v)?; } @@ -21925,13 +21905,11 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackResponse { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "handle", "info", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Handle, Info, __SkipField__, } @@ -21955,7 +21933,6 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackResponse { E: serde::de::Error, { match value { - "handle" => Ok(GeneratedField::Handle), "info" => Ok(GeneratedField::Info), _ => Ok(GeneratedField::__SkipField__), } @@ -21976,18 +21953,9 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackResponse { where V: serde::de::MapAccess<'de>, { - let mut handle__ = None; let mut info__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Handle => { - if handle__.is_some() { - return Err(serde::de::Error::duplicate_field("handle")); - } - handle__ = - Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) - ; - } GeneratedField::Info => { if info__.is_some() { return Err(serde::de::Error::duplicate_field("info")); @@ -22000,7 +21968,6 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackResponse { } } Ok(PublishDataTrackResponse { - handle: handle__.unwrap_or_default(), info: info__, }) } @@ -24999,10 +24966,10 @@ impl serde::Serialize for request_response::Reason { Self::Queued => "QUEUED", Self::UnsupportedType => "UNSUPPORTED_TYPE", Self::UnclassifiedError => "UNCLASSIFIED_ERROR", - Self::DataTrackInvalidHandle => "DATA_TRACK_INVALID_HANDLE", - Self::DataTrackInvalidName => "DATA_TRACK_INVALID_NAME", - Self::DataTrackInvalidMimeType => "DATA_TRACK_INVALID_MIME_TYPE", - Self::DataTrackNameTaken => "DATA_TRACK_NAME_TAKEN", + Self::InvalidHandle => "INVALID_HANDLE", + Self::InvalidName => "INVALID_NAME", + Self::DuplicateHandle => "DUPLICATE_HANDLE", + Self::DuplicateName => "DUPLICATE_NAME", }; serializer.serialize_str(variant) } @@ -25021,10 +24988,10 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "QUEUED", "UNSUPPORTED_TYPE", "UNCLASSIFIED_ERROR", - "DATA_TRACK_INVALID_HANDLE", - "DATA_TRACK_INVALID_NAME", - "DATA_TRACK_INVALID_MIME_TYPE", - "DATA_TRACK_NAME_TAKEN", + "INVALID_HANDLE", + "INVALID_NAME", + "DUPLICATE_HANDLE", + "DUPLICATE_NAME", ]; struct GeneratedVisitor; @@ -25072,10 +25039,10 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "QUEUED" => Ok(request_response::Reason::Queued), "UNSUPPORTED_TYPE" => Ok(request_response::Reason::UnsupportedType), "UNCLASSIFIED_ERROR" => Ok(request_response::Reason::UnclassifiedError), - "DATA_TRACK_INVALID_HANDLE" => Ok(request_response::Reason::DataTrackInvalidHandle), - "DATA_TRACK_INVALID_NAME" => Ok(request_response::Reason::DataTrackInvalidName), - "DATA_TRACK_INVALID_MIME_TYPE" => Ok(request_response::Reason::DataTrackInvalidMimeType), - "DATA_TRACK_NAME_TAKEN" => Ok(request_response::Reason::DataTrackNameTaken), + "INVALID_HANDLE" => Ok(request_response::Reason::InvalidHandle), + "INVALID_NAME" => Ok(request_response::Reason::InvalidName), + "DUPLICATE_HANDLE" => Ok(request_response::Reason::DuplicateHandle), + "DUPLICATE_NAME" => Ok(request_response::Reason::DuplicateName), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -40479,12 +40446,12 @@ impl serde::Serialize for UnpublishDataTrackRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if self.handle != 0 { + if self.pub_handle != 0 { len += 1; } let mut struct_ser = serializer.serialize_struct("livekit.UnpublishDataTrackRequest", len)?; - if self.handle != 0 { - struct_ser.serialize_field("handle", &self.handle)?; + if self.pub_handle != 0 { + struct_ser.serialize_field("pubHandle", &self.pub_handle)?; } struct_ser.end() } @@ -40496,12 +40463,13 @@ impl<'de> serde::Deserialize<'de> for UnpublishDataTrackRequest { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "handle", + "pub_handle", + "pubHandle", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Handle, + PubHandle, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -40524,7 +40492,7 @@ impl<'de> serde::Deserialize<'de> for UnpublishDataTrackRequest { E: serde::de::Error, { match value { - "handle" => Ok(GeneratedField::Handle), + "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), _ => Ok(GeneratedField::__SkipField__), } } @@ -40544,14 +40512,14 @@ impl<'de> serde::Deserialize<'de> for UnpublishDataTrackRequest { where V: serde::de::MapAccess<'de>, { - let mut handle__ = None; + let mut pub_handle__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Handle => { - if handle__.is_some() { - return Err(serde::de::Error::duplicate_field("handle")); + GeneratedField::PubHandle => { + if pub_handle__.is_some() { + return Err(serde::de::Error::duplicate_field("pubHandle")); } - handle__ = + pub_handle__ = Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) ; } @@ -40561,7 +40529,7 @@ impl<'de> serde::Deserialize<'de> for UnpublishDataTrackRequest { } } Ok(UnpublishDataTrackRequest { - handle: handle__.unwrap_or_default(), + pub_handle: pub_handle__.unwrap_or_default(), }) } } @@ -40576,12 +40544,12 @@ impl serde::Serialize for UnpublishDataTrackResponse { { use serde::ser::SerializeStruct; let mut len = 0; - if self.handle != 0 { + if self.pub_handle != 0 { len += 1; } let mut struct_ser = serializer.serialize_struct("livekit.UnpublishDataTrackResponse", len)?; - if self.handle != 0 { - struct_ser.serialize_field("handle", &self.handle)?; + if self.pub_handle != 0 { + struct_ser.serialize_field("pubHandle", &self.pub_handle)?; } struct_ser.end() } @@ -40593,12 +40561,13 @@ impl<'de> serde::Deserialize<'de> for UnpublishDataTrackResponse { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "handle", + "pub_handle", + "pubHandle", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Handle, + PubHandle, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -40621,7 +40590,7 @@ impl<'de> serde::Deserialize<'de> for UnpublishDataTrackResponse { E: serde::de::Error, { match value { - "handle" => Ok(GeneratedField::Handle), + "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), _ => Ok(GeneratedField::__SkipField__), } } @@ -40641,14 +40610,14 @@ impl<'de> serde::Deserialize<'de> for UnpublishDataTrackResponse { where V: serde::de::MapAccess<'de>, { - let mut handle__ = None; + let mut pub_handle__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Handle => { - if handle__.is_some() { - return Err(serde::de::Error::duplicate_field("handle")); + GeneratedField::PubHandle => { + if pub_handle__.is_some() { + return Err(serde::de::Error::duplicate_field("pubHandle")); } - handle__ = + pub_handle__ = Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) ; } @@ -40658,7 +40627,7 @@ impl<'de> serde::Deserialize<'de> for UnpublishDataTrackResponse { } } Ok(UnpublishDataTrackResponse { - handle: handle__.unwrap_or_default(), + pub_handle: pub_handle__.unwrap_or_default(), }) } } @@ -40771,6 +40740,9 @@ impl serde::Serialize for update_data_subscription::Update { if !self.sid.is_empty() { len += 1; } + if !self.participant_identity.is_empty() { + len += 1; + } if self.subscribe { len += 1; } @@ -40781,6 +40753,9 @@ impl serde::Serialize for update_data_subscription::Update { if !self.sid.is_empty() { struct_ser.serialize_field("sid", &self.sid)?; } + if !self.participant_identity.is_empty() { + struct_ser.serialize_field("participantIdentity", &self.participant_identity)?; + } if self.subscribe { struct_ser.serialize_field("subscribe", &self.subscribe)?; } @@ -40798,6 +40773,8 @@ impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { { const FIELDS: &[&str] = &[ "sid", + "participant_identity", + "participantIdentity", "subscribe", "options", ]; @@ -40805,6 +40782,7 @@ impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { #[allow(clippy::enum_variant_names)] enum GeneratedField { Sid, + ParticipantIdentity, Subscribe, Options, __SkipField__, @@ -40830,6 +40808,7 @@ impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { { match value { "sid" => Ok(GeneratedField::Sid), + "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), "subscribe" => Ok(GeneratedField::Subscribe), "options" => Ok(GeneratedField::Options), _ => Ok(GeneratedField::__SkipField__), @@ -40852,6 +40831,7 @@ impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { V: serde::de::MapAccess<'de>, { let mut sid__ = None; + let mut participant_identity__ = None; let mut subscribe__ = None; let mut options__ = None; while let Some(k) = map_.next_key()? { @@ -40862,6 +40842,12 @@ impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { } sid__ = Some(map_.next_value()?); } + GeneratedField::ParticipantIdentity => { + if participant_identity__.is_some() { + return Err(serde::de::Error::duplicate_field("participantIdentity")); + } + participant_identity__ = Some(map_.next_value()?); + } GeneratedField::Subscribe => { if subscribe__.is_some() { return Err(serde::de::Error::duplicate_field("subscribe")); @@ -40881,6 +40867,7 @@ impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { } Ok(update_data_subscription::Update { sid: sid__.unwrap_or_default(), + participant_identity: participant_identity__.unwrap_or_default(), subscribe: subscribe__.unwrap_or_default(), options: options__, }) From 12a7ee3efb378f311dda1e3354ba9a64097f835a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:50:03 +1100 Subject: [PATCH 010/232] Track handle assignment & conversion --- livekit-datatrack/src/dtp/deserialize.rs | 2 +- livekit-datatrack/src/dtp/serialize.rs | 2 +- livekit-datatrack/src/dtp/track_handle.rs | 33 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs index c19107b61..1932d6b65 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -190,7 +190,7 @@ mod tests { let dtp = Dtp::deserialize(raw.freeze()).unwrap(); assert_eq!(dtp.header.version, 0); assert_eq!(dtp.header.is_final, true); - assert_eq!(dtp.header.track_handle, 0x8811.try_into().unwrap()); + assert_eq!(dtp.header.track_handle, 0x8811u32.try_into().unwrap()); assert_eq!(dtp.header.sequence, 0x4422); assert_eq!(dtp.header.frame_number, 0x4411); assert_eq!(dtp.header.timestamp, 0x44221188); diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/dtp/serialize.rs index 416fc277d..5f2fe3579 100644 --- a/livekit-datatrack/src/dtp/serialize.rs +++ b/livekit-datatrack/src/dtp/serialize.rs @@ -147,7 +147,7 @@ mod tests { header: Header { version: 0, is_final: true, - track_handle: 0x8811.try_into().unwrap(), + track_handle: 0x8811u32.try_into().unwrap(), sequence: 0x4422, frame_number: 0x4411, timestamp: 0x44221188, diff --git a/livekit-datatrack/src/dtp/track_handle.rs b/livekit-datatrack/src/dtp/track_handle.rs index 9bf7a1724..9b311a420 100644 --- a/livekit-datatrack/src/dtp/track_handle.rs +++ b/livekit-datatrack/src/dtp/track_handle.rs @@ -23,6 +23,9 @@ pub struct TrackHandle(u16); pub enum TrackHandleError { #[error("{0:#X} is reserved")] Reserved(u16), + + #[error("value too large to be a valid track handle")] + TooLarge, } impl TryFrom for TrackHandle { @@ -36,14 +39,44 @@ impl TryFrom for TrackHandle { } } +impl TryFrom for TrackHandle { + type Error = TrackHandleError; + + fn try_from(value: u32) -> Result { + let value: u16 = value.try_into().map_err(|_| TrackHandleError::TooLarge)?; + Ok(value.try_into()?) + } +} + impl Into for TrackHandle { fn into(self) -> u16 { self.0 } } +impl Into for TrackHandle { + fn into(self) -> u32 { + self.0 as u32 + } +} + impl Display for TrackHandle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{0:#X}", self.0) } } + +/// Utility for allocating unique track handles to use for publishing tracks. +#[derive(Debug, Default)] +pub struct TrackHandleAllocator { + /// Next handle value. + value: u16, +} + +impl TrackHandleAllocator { + /// Returns a unique track handle for the next publication, if one can be obtained. + pub fn get(&mut self) -> Option { + let value = self.value.checked_add(1)?; + TrackHandle(value).into() + } +} From c70a60672d5a86a9f45ade40b53092e1ceb4ab48 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 11:56:38 +1100 Subject: [PATCH 011/232] Remove mime Not in scope for v1 --- livekit-datatrack/src/mime.rs | 107 ---------------------------------- 1 file changed, 107 deletions(-) delete mode 100644 livekit-datatrack/src/mime.rs diff --git a/livekit-datatrack/src/mime.rs b/livekit-datatrack/src/mime.rs deleted file mode 100644 index 615418f18..000000000 --- a/livekit-datatrack/src/mime.rs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2025 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 std::borrow::Cow; -use std::fmt; -use std::str::FromStr; - -#[derive(PartialEq, Eq, Clone, Debug)] -pub struct Mime(Cow<'static, str>); - -impl Mime { - pub const BINARY: Mime = Mime(Cow::Borrowed("application/octet-stream")); - pub const PLAIN_TEXT: Mime = Mime(Cow::Borrowed("text/plain")); - pub const JSON: Mime = Mime(Cow::Borrowed("application/json")); - pub const YAML: Mime = Mime(Cow::Borrowed("application/yaml")); - pub const PROTOBUF: Mime = Mime(Cow::Borrowed("application/x-protobuf")); -} - -impl fmt::Display for Mime { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum MimeError { - #[error("Empty string")] - Empty, - #[error("Unknown top level type")] - UnknownTopLevelType, - #[error("Missing sub-type")] - MissingSubtype, - #[error("Sub-type is empty or contains invalid characters")] - InvalidSubtype, -} - -impl FromStr for Mime { - type Err = MimeError; - fn from_str(s: &str) -> Result { - Self::validate(s)?; - let mime = Mime(Cow::Owned(s.to_string())); - Ok(mime) - } -} - -impl Mime { - - fn validate(s: &str) -> Result<(), MimeError> { - if s.is_empty() { - Err(MimeError::Empty)? - } - let (top_level_type, sub_type) = s.split_once("/").ok_or(MimeError::MissingSubtype)?; - if !Self::TOP_LEVEL_TYPES.contains(&top_level_type) { - Err(MimeError::UnknownTopLevelType)? - } - if !Self::is_valid_subtype(sub_type) { - Err(MimeError::InvalidSubtype)? - } - Ok(()) - } - - /// Defined top level media types: - /// https://www.iana.org/assignments/top-level-media-types/top-level-media-types.xhtml - const TOP_LEVEL_TYPES: &[&str] = &[ - "application", - "text", - "image", - "audio", - "video", - "multipart", - "message", - "model", - "haptics", - "example", - "font", - ]; - - fn is_valid_subtype(sub_type: &str) -> bool { - !sub_type.is_empty() - && sub_type.chars().all(|c| match c { - 'a'..='z' - | 'A'..='Z' - | '0'..='9' - | '!' - | '#' - | '$' - | '&' - | '^' - | '_' - | '.' - | '+' - | '-' => true, - _ => false, - }) - } -} \ No newline at end of file From 0975b244a992001e519c0f6fc2c72997e20ebe9f Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:03:06 +1100 Subject: [PATCH 012/232] Publish implementation --- Cargo.lock | 43 ++- livekit-datatrack/Cargo.toml | 7 +- livekit-datatrack/src/error.rs | 69 +++- livekit-datatrack/src/frame.rs | 16 +- livekit-datatrack/src/lib.rs | 5 +- livekit-datatrack/src/manager/e2ee.rs | 30 ++ livekit-datatrack/src/manager/mod.rs | 7 +- livekit-datatrack/src/manager/publish.rs | 390 ++++++++++++++++++++--- livekit-datatrack/src/track.rs | 96 +++--- 9 files changed, 540 insertions(+), 123 deletions(-) create mode 100644 livekit-datatrack/src/manager/e2ee.rs diff --git a/Cargo.lock b/Cargo.lock index ea9cfd1dd..c73d0aaf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1271,8 +1271,8 @@ dependencies = [ ] [[package]] -name = "dashmap" -version = "5.5.3" +name = "darling" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ @@ -1281,16 +1281,42 @@ dependencies = [ "lock_api", "once_cell", "parking_lot_core", + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", ] [[package]] name = "dashmap" -version = "6.1.0" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "crossbeam-utils", "hashbrown 0.14.3", "lock_api", "once_cell", @@ -3041,13 +3067,16 @@ dependencies = [ name = "livekit-datatrack" version = "0.1.0" dependencies = [ + "anyhow", "bytes", - "dashmap 6.1.0", + "from_variants", "futures-util", "livekit-protocol", + "livekit-runtime", "log", "thiserror 2.0.17", "tokio", + "tokio-stream", ] [[package]] @@ -3056,7 +3085,7 @@ version = "0.12.39" dependencies = [ "bytes", "console-subscriber", - "dashmap 5.5.3", + "dashmap", "downcast-rs", "env_logger 0.10.2", "from_variants", diff --git a/livekit-datatrack/Cargo.toml b/livekit-datatrack/Cargo.toml index c8100c43b..d366b2538 100644 --- a/livekit-datatrack/Cargo.toml +++ b/livekit-datatrack/Cargo.toml @@ -7,10 +7,13 @@ repository = "https://github.com/livekit/rust-sdks" readme = "README.md" [dependencies] -dashmap = "6.1.0" livekit-protocol = { workspace = true } -log = "0.4.28" +livekit-runtime = { workspace = true } +log = { version = "0.4.28" } thiserror = "2.0.17" tokio = { version = "1.48.0", default-features = false, features = ["sync"] } futures-util = { version = "0.3", default-features = false, features = ["sink"] } bytes = "1.10.1" +from_variants = "1.0.2" +tokio-stream = "0.1.17" +anyhow = "1.0.100" # For internal error handling only diff --git a/livekit-datatrack/src/error.rs b/livekit-datatrack/src/error.rs index b4f6f142a..63ddd7cb3 100644 --- a/livekit-datatrack/src/error.rs +++ b/livekit-datatrack/src/error.rs @@ -12,12 +12,71 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::frame::DataTrackFrame; +use core::fmt; use thiserror::Error; #[derive(Debug, Error)] pub enum PublishError { - #[error("")] // TODO: descriptions - NameTaken, - #[error("")] - Unauthorized, -} \ No newline at end of file + #[error("The local participant does not have permission to publish data tracks")] + NotAllowed, + #[error("A data track with the same name is already published by the local participant")] + DuplicateName, + #[error("Request to publish data track timed-out")] + Timeout, + #[error("No more data tracks are able to be published")] + LimitReached, + #[error(transparent)] + Internal(#[from] InternalError), +} + +#[derive(Debug, Error)] +pub enum SubscribeError {} + +/// An error that can occur when publishing a frame onto a data track. +#[derive(Debug, Error)] +#[error("Failed to publish frame: {reason}")] +pub struct PublishFrameError { + frame: DataTrackFrame, + reason: PublishFrameErrorReason, +} + +impl PublishFrameError { + pub(crate) fn new(frame: DataTrackFrame, reason: PublishFrameErrorReason) -> Self { + Self { frame, reason } + } + + /// Consume the error, returning the frame that couldn't be published. + pub fn into_frame(self) -> DataTrackFrame { + self.frame + } + + /// Returns the reason why the frame could not be published. + pub fn reason(&self) -> PublishFrameErrorReason { + self.reason + } +} + +/// Reason why a data track frame could not be published. +#[derive(Debug, Clone, Copy)] +pub enum PublishFrameErrorReason { + /// Track is no longer published. + TrackUnpublished, + /// Frame was dropped. + Dropped, +} +// TODO: could provide unpublish reason and more +// info about why the frame was dropped. + +impl fmt::Display for PublishFrameErrorReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TrackUnpublished => write!(f, "track unpublished"), + Self::Dropped => write!(f, "dropped"), + } + } +} + +#[derive(Debug, Error)] +#[error(transparent)] +pub struct InternalError(#[from] anyhow::Error); diff --git a/livekit-datatrack/src/frame.rs b/livekit-datatrack/src/frame.rs index d956e0f89..5d837e92f 100644 --- a/livekit-datatrack/src/frame.rs +++ b/livekit-datatrack/src/frame.rs @@ -14,13 +14,18 @@ use bytes::Bytes; +#[derive(Debug)] pub struct DataTrackFrame { - payload: Bytes, + pub(crate) payload: Bytes, + pub(crate) user_timestamp: Option, } impl DataTrackFrame { - pub fn payload(&self) -> &Bytes { - &self.payload + pub fn payload(&self) -> Bytes { + self.payload.clone() // Cheap clone + } + pub fn user_timestamp(&self) -> Option { + self.user_timestamp } } @@ -41,6 +46,9 @@ impl DataTrackFrameBuilder { } pub fn build(self) -> DataTrackFrame { - todo!() + DataTrackFrame { + payload: self.payload, + user_timestamp: self.user_timestamp + } } } diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index 4715eee1a..5dae100c2 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -13,8 +13,9 @@ // limitations under the License. mod dtp; -mod mime; mod frame; mod track; mod error; -mod manager; \ No newline at end of file +mod manager; + +pub use manager::*; \ No newline at end of file diff --git a/livekit-datatrack/src/manager/e2ee.rs b/livekit-datatrack/src/manager/e2ee.rs new file mode 100644 index 000000000..a605db6fe --- /dev/null +++ b/livekit-datatrack/src/manager/e2ee.rs @@ -0,0 +1,30 @@ +// Copyright 2025 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 std::fmt::Debug; +use bytes::Bytes; + +pub struct EncryptedPayload { + pub payload: Bytes, + pub iv: [u8; 12], + pub key_index: u8 +} + +pub trait EncryptionProvider: Debug { + fn encrypt(&self, payload: Bytes) -> EncryptedPayload; +} + +pub trait DecryptionProvider: Debug { + fn decrypt(&self, payload: EncryptedPayload) -> Bytes; +} \ No newline at end of file diff --git a/livekit-datatrack/src/manager/mod.rs b/livekit-datatrack/src/manager/mod.rs index 6cd39c9c0..503b39d60 100644 --- a/livekit-datatrack/src/manager/mod.rs +++ b/livekit-datatrack/src/manager/mod.rs @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod e2ee; pub mod publish; -pub mod subscribe; +// pub mod subscribe; -#[derive(PartialEq, Eq, Hash, Debug)] -pub struct TrackHandle(u16); \ No newline at end of file +pub use publish::*; +// pub use subscribe::*; \ No newline at end of file diff --git a/livekit-datatrack/src/manager/publish.rs b/livekit-datatrack/src/manager/publish.rs index 54437a61a..a588976b6 100644 --- a/livekit-datatrack/src/manager/publish.rs +++ b/livekit-datatrack/src/manager/publish.rs @@ -12,96 +12,386 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{error::PublishError, track::{DataTrack, Local, PublishOptions}}; +use crate::{ + dtp::TrackHandleAllocator, + error::{InternalError, PublishError, PublishFrameError, PublishFrameErrorReason}, + frame::DataTrackFrame, + manager::e2ee::EncryptionProvider, + track::{DataTrack, DataTrackInfo, Local, PublishOptions, TrackHandle}, +}; +use anyhow::{anyhow, Context}; +use bytes::Bytes; +use from_variants::FromVariants; +use futures_util::Stream; +use livekit_protocol::{self as proto}; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use tokio::{ + sync::{mpsc, oneshot, watch}, + time::timeout, +}; +use tokio_stream::wrappers::ReceiverStream; -use super::TrackHandle; -use dashmap::DashMap; -use livekit_protocol as proto; -use tokio::sync::mpsc; +#[derive(Debug, Clone, Copy)] +pub enum DataTrackState { + Published, + Unpublished { sfu_initiated: bool }, +} + +#[derive(Debug, Clone)] +pub(crate) struct PubHandle { + frame_tx: mpsc::Sender, + state_tx: watch::Sender, +} +impl PubHandle { + pub fn publish(&self, frame: DataTrackFrame) -> Result<(), PublishFrameError> { + if !self.is_published() { + return Err(PublishFrameError::new(frame, PublishFrameErrorReason::TrackUnpublished)); + } + self.frame_tx.try_send(frame).map_err(|err| { + PublishFrameError::new(err.into_inner(), PublishFrameErrorReason::Dropped) + }) + } + pub fn is_published(&self) -> bool { + matches!(*self.state_tx.borrow(), DataTrackState::Published) + } -// Question: mechanism for signaling tx/rx. Options: -// 1. -// 2. + pub fn unpublish(&self) { + self.state_tx + .send(DataTrackState::Unpublished { sfu_initiated: false }) + .inspect_err(|err| log::error!("Failed to update state to unsubscribed: {err}")) + .ok(); + } +} -enum PubMangerRxMessage { - PublishedResponse(proto::DataTrackPublishedResponse), - UnpublishedResponse(()), - RequestResponse(proto::RequestResponse) +impl Drop for PubHandle { + fn drop(&mut self) { + // Implicit unpublish when handle dropped. + self.unpublish(); + } } -enum PubMangerTxMessage { - AddTrack(proto::AddTrackRequest), - UnpublishRequest(()) +struct TrackPubTask { + // TODO: packetizer, e2ee_provider, rate tracking, etc. + info: Arc, + state_rx: watch::Receiver, + frame_rx: mpsc::Receiver, + packet_out_tx: mpsc::Sender, + signal_out_tx: mpsc::Sender, +} + +impl TrackPubTask { + async fn run(mut self) -> Result<(), InternalError> { + let mut state = DataTrackState::Published; + while matches!(state, DataTrackState::Published) { + tokio::select! { + _ = self.state_rx.changed() => Ok(state = *self.state_rx.borrow()), + Some(frame) = self.frame_rx.recv() => self.packetize_and_send(frame), + else => break + } + .inspect_err(|err| log::error!("{}", err)) + .ok(); + } + if let DataTrackState::Unpublished { sfu_initiated } = state { + if !sfu_initiated { + self.send_unpublish_req()?; + } + } + Ok(()) + } + + fn packetize_and_send(&self, frame: DataTrackFrame) -> Result<(), InternalError> { + todo!() + } + + fn send_unpublish_req(self) -> Result<(), InternalError> { + let req = proto::UnpublishDataTrackRequest { pub_handle: self.info.handle.into() }; + Ok(self.signal_out_tx.try_send(req.into()).context("Failed to send unpublish")?) + } } #[derive(Debug)] pub struct PubManagerOptions { - // Dependencies: - // - E2EE - // - Signaling - // - Tx AddTrackRequest, UnpublishDataTrackRequest - // - Rx DataTrackPublishedResponse, DataTrackUnpublishedResponse, RequestResponse - // - Data track channel - // - Tx - transport_tx: mpsc::UnboundedReceiver> + pub encryption: Option>, } -#[derive(Debug)] +/// Manager for data track publications. +#[derive(Debug, Clone)] pub struct PubManager { - options: PubManagerOptions, - pub_tracks: DashMap, + signal_in_tx: mpsc::Sender, + pub_req_tx: mpsc::Sender, } impl PubManager { - pub fn new(options: PubManagerOptions) -> Self { - Self { options, pub_tracks: DashMap::default() } + const CH_BUFFER_SIZE: usize = 4; + const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); + + pub fn new( + options: PubManagerOptions, + ) -> (Self, PubManagerTask, impl Stream, impl Stream) + { + let (pub_req_tx, pub_req_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); + let (signal_in_tx, signal_in_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); + let (signal_out_tx, signal_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); + let (packet_out_tx, packet_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); + + let manager = Self { signal_in_tx, pub_req_tx }; + let task = PubManagerTask { + encryption: options.encryption, + pub_req_rx, + signal_in_rx, + signal_out_tx, + packet_out_tx, + handle_allocator: TrackHandleAllocator::default(), + pending_publications: HashMap::new(), + active_publications: HashMap::new(), + }; + + let signal_out_stream = ReceiverStream::new(signal_out_rx); + let packet_out_stream = ReceiverStream::new(packet_out_rx); + + (manager, task, signal_out_stream, packet_out_stream) } - // from track published, participant update? - pub fn update_state() { - todo!() + /// Handles a signal message from the SFU. + /// + /// In order to function correctly, all message types enumerated in [`PubSignalInput`] + /// must be forwarded here. + /// + pub fn handle_signal(&self, message: PubSignalInput) -> Result<(), InternalError> { + Ok(self.signal_in_tx.try_send(message).context("Failed to handle signal input")?) + } + + /// Publishes a data track with the given options. + pub async fn publish_track( + &self, + options: PublishOptions, + ) -> Result, PublishError> { + let (result_tx, result_rx) = oneshot::channel(); + let request = PubRequest { options, result_tx }; + self.pub_req_tx.send(request); + + // TODO: move timeout inside pub manager + let track = timeout(Self::PUBLISH_TIMEOUT, result_rx) + .await + .map_err(|_| PublishError::Timeout)? + .map_err(|_| PublishError::Internal(anyhow!("Request rx closed").into()))??; + Ok(track) } +} - pub async fn publish(options: PublishOptions) -> Result, PublishError> { - let request = options.into_add_track_request(false); // set based on E2EE options +pub struct PubManagerTask { + encryption: Option>, + pub_req_rx: mpsc::Receiver, + signal_in_rx: mpsc::Receiver, + signal_out_tx: mpsc::Sender, + packet_out_tx: mpsc::Sender, + handle_allocator: TrackHandleAllocator, + pending_publications: + HashMap, PublishError>>>, + active_publications: HashMap>, +} - // TODO: send request, await response - let response = proto::DataTrackPublishedResponse::default(); +impl PubManagerTask { + pub async fn run(mut self) -> Result<(), InternalError> { + loop { + tokio::select! { + biased; // Handle signal input before publish requests. + // TODO: check cancellation + Some(signal) = self.signal_in_rx.recv() => self.handle_signal(signal), + //Some(unpublish_req) = self.unpub_req_rx.recv() => self.handle_unpublish_req(unpublish_req), + Some(publish_req) = self.pub_req_rx.recv() => self.handle_publish_req(publish_req), + else => Ok(()) + } + .inspect_err(|err| log::error!("{}", err)) + .ok(); + } + } + fn handle_publish_req(&mut self, req: PubRequest) -> Result<(), InternalError> { + let Some(handle) = self.handle_allocator.get() else { + req.result_tx.send(Err(PublishError::LimitReached)); + return Ok(()); + }; - todo!() + if !self.pending_publications.insert(handle, req.result_tx).is_some() { + Err(anyhow!("Publication already pending for handle"))? + } + + let use_e2ee = self.encryption.is_some() && !req.options.disable_e2ee; + let request = req.options.into_add_track_request(use_e2ee, handle); + self.signal_out_tx.try_send(request.into()).context("Failed to send add track")?; + Ok(()) } - // handle update + fn handle_signal(&mut self, message: PubSignalInput) -> Result<(), InternalError> { + match message { + PubSignalInput::PublishResponse(res) => self.handle_publish_response(res), + PubSignalInput::UnpublishResponse(res) => self.handle_unpublish_response(res), + PubSignalInput::RequestResponse(res) => self.handle_request_response(res), + } + } + + fn handle_publish_response( + &mut self, + res: proto::PublishDataTrackResponse, + ) -> Result<(), InternalError> { + let info: DataTrackInfo = res.try_into()?; + let Some(res_tx) = self.pending_publications.remove(&info.handle) else { + Err(anyhow!("No pending track publication for {}", info.handle))? + }; + let track = self.create_local_track(info); + res_tx.send(Ok(track)); + Ok(()) + } + + fn create_local_track(&mut self, info: DataTrackInfo) -> DataTrack { + let (frame_tx, frame_rx) = mpsc::channel(4); // TODO: tune + let (state_tx, state_rx) = watch::channel(DataTrackState::Published); + let info = Arc::new(info); + + let task = TrackPubTask { + // TODO: handle cancellation + info: info.clone(), + frame_rx, + state_rx, + packet_out_tx: self.packet_out_tx.clone(), + signal_out_tx: self.signal_out_tx.clone(), + }; + livekit_runtime::spawn(task.run()); + self.active_publications.insert(info.handle, state_tx.clone()); + + let handle = PubHandle { frame_tx, state_tx }; + DataTrack::new(info, handle) + } + + fn handle_request_response( + &mut self, + res: proto::RequestResponse, + ) -> Result<(), InternalError> { + let reason = res.reason(); + let req = res.request.context("Missing request")?; + use proto::request_response::Request; + match req { + Request::PublishDataTrack(req) => { + let handle: TrackHandle = + req.pub_handle.try_into().context("Invalid track handle")?; + let Some(res_tx) = self.pending_publications.remove(&handle) else { + Err(anyhow!("No pending publication for {}", req.pub_handle))? + }; + let error: PublishError = reason.into(); + _ = res_tx.send(Err(error)); + } + Request::UnpublishDataTrack(req) => { + log::warn!("Unpublish failed for {}", req.pub_handle) + } + _ => {} // Not handled by this module + } + Ok(()) + } + + fn handle_unpublish_response( + &mut self, + res: proto::UnpublishDataTrackResponse, + ) -> Result<(), InternalError> { + let handle: TrackHandle = res.pub_handle.try_into().context("Invalid handle")?; + let Some(state_tx) = self.active_publications.remove(&handle) else { + Err(anyhow!("Cannot handle unpublish for unknown track {}", handle))? + }; + let state = *state_tx.borrow(); + match state { + DataTrackState::Published => { + state_tx + .send(DataTrackState::Unpublished { sfu_initiated: true }) + .context("Failed to set state")?; + } + DataTrackState::Unpublished { sfu_initiated } => { + if sfu_initiated { + Err(anyhow!("Received unpublish response for same track more than once"))? + } + } + } + Ok(()) + } } -#[derive(Debug)] -struct Descriptor { - // minimal info - // stats (fps calculation, total sent, etc.) - // rx - // publish -> dtp encode -> data channel +struct PubRequest { + options: PublishOptions, + result_tx: oneshot::Sender, PublishError>>, } -impl Into for PublishOptions { - fn into(self) -> proto::AddDataTrackRequest { - todo!() - } +#[derive(Debug, FromVariants)] +pub enum PubManagerInput { + Signal(PubSignalInput), + Transport(Bytes), +} + +/// Signal message produced by [`PubManager`] to be forwarded to the SFU. +#[derive(Debug, FromVariants)] +pub enum PubSignalOutput { + PublishRequest(proto::PublishDataTrackRequest), + UnpublishRequest(proto::UnpublishDataTrackRequest), +} + +/// Signal message received from the SFU handled by [`PubManager`]. +#[derive(Debug, FromVariants)] +pub enum PubSignalInput { + PublishResponse(proto::PublishDataTrackResponse), + UnpublishResponse(proto::UnpublishDataTrackResponse), + RequestResponse(proto::RequestResponse), } impl PublishOptions { - fn into_add_track_request(self, use_e2ee: bool) -> proto::AddDataTrackRequest { + fn into_add_track_request( + self, + use_e2ee: bool, + handle: TrackHandle, + ) -> proto::PublishDataTrackRequest { let encryption = if self.disable_e2ee || !use_e2ee { proto::encryption::Type::None } else { proto::encryption::Type::Gcm }; - proto::AddDataTrackRequest { + proto::PublishDataTrackRequest { + pub_handle: handle.into(), name: self.name, - mime_type: self.mime.to_string(), encryption: encryption.into(), } } } + +impl Into for proto::request_response::Reason { + fn into(self) -> PublishError { + use proto::request_response::Reason; + // If new error cases are added in the future, consider if they should + // be treated as internal errors or added to the public error enum. + match self { + Reason::NotAllowed => PublishError::NotAllowed, + Reason::DuplicateName => PublishError::DuplicateName, + other => PublishError::Internal(anyhow!("SFU rejected: {:?}", other).into()), + } + } +} + +impl TryInto for proto::PublishDataTrackResponse { + type Error = anyhow::Error; + fn try_into(self) -> Result { + let info = self.info.context("Missing info")?; + info.try_into() + } +} + +impl TryInto for proto::DataTrackInfo { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + let handle: TrackHandle = self.pub_handle.try_into()?; + let uses_e2ee = match self.encryption() { + proto::encryption::Type::None => false, + proto::encryption::Type::Gcm => true, + other => Err(anyhow!("Unsupported E2EE type: {:?}", other))?, + }; + Ok(DataTrackInfo { handle, sid: self.sid, name: self.name, uses_e2ee }) + } +} diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index aa44e63bb..65bf1e8d9 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -12,39 +12,38 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{frame::DataTrackFrame, mime::Mime}; -use futures_util::{task::Context, Stream}; -use std::{marker::PhantomData, pin::Pin, task::Poll}; +use crate::{ + error::PublishFrameError, + frame::DataTrackFrame, + manager::{self}, +}; +use from_variants::FromVariants; +use std::{marker::PhantomData, sync::Arc}; + +pub use crate::dtp::TrackHandle; /// Options for publishing a data track. #[derive(Clone, Debug)] pub struct PublishOptions { pub(crate) name: String, pub(crate) disable_e2ee: bool, - pub(crate) mime: Mime, } impl PublishOptions { pub fn with_name(name: impl Into) -> Self { - Self { name: name.into(), disable_e2ee: false, mime: Mime::BINARY } - } - - pub fn mime(self, mime: Mime) -> Self { - Self { mime, ..self } + Self { name: name.into(), disable_e2ee: false } } - pub fn disable_e2ee(self, disabled: bool) -> Self { Self { disable_e2ee: disabled, ..self } } } -#[derive(Clone, Debug)] -struct DataTrackInfo { - sid: String, // TODO: use shared ID type - handle: u16, - name: String, - mime: Mime, - uses_e2ee: bool, +#[derive(Debug, Clone)] +pub struct DataTrackInfo { + pub(crate) sid: String, // TODO: use shared ID type + pub(crate) handle: TrackHandle, + pub(crate) name: String, + pub(crate) uses_e2ee: bool, } impl DataTrackInfo { @@ -54,69 +53,66 @@ impl DataTrackInfo { pub fn name(&self) -> &str { &self.name } - pub fn mime(&self) -> &Mime { - &self.mime - } pub fn uses_e2ee(&self) -> bool { self.uses_e2ee } } /// Marker type indicating a [`DataTrack`] belongs to the local participant. +#[derive(Debug)] pub struct Local; /// Marker type indicating a [`DataTrack`] belongs to a remote participant. +#[derive(Debug)] pub struct Remote; -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] pub struct DataTrack { + info: Arc, + inner: DataTrackInner, /// Marker indicating local or remote. _location: PhantomData, - // Need info, way to signal closing by SFU or other +} - // Cases: - // Local (publish) -> channel tx - // Remote (subscribe) -> channel rx +#[derive(Debug, Clone, FromVariants)] +enum DataTrackInner { + Local(manager::PubHandle), + Remote(()), // TODO: add sub handle } impl DataTrack { - pub fn info(&self) -> DataTrackInfo { - todo!() + /// Information about the data track such as name. + pub fn info(&self) -> &DataTrackInfo { + &self.info } } impl DataTrack { - pub fn publish(&self, frame: impl Into) -> DataTrackResult<()> { - todo!() + pub(crate) fn new(info: Arc, handle: manager::PubHandle) -> Self { + Self { info, inner: handle.into(), _location: PhantomData } } -} -impl DataTrack { - pub(crate) fn from_info(info: DataTrackInfo) -> Result { - Ok(Self { _location: PhantomData }) + fn handle(&self) -> &manager::PubHandle { + match &self.inner { + DataTrackInner::Local(publisher) => publisher, + DataTrackInner::Remote(_) => unreachable!(), // Safe (type state) + } } - pub fn is_subscribed() -> bool { - // Subscribed as long as there is at least one subscription - todo!() + /// Publish a frame onto the track. + pub fn publish(&self, frame: impl Into) -> Result<(), PublishFrameError> { + Ok(self.handle().publish(frame.into())?) } - pub fn subscribe(&self) -> DataTrackResult { - // TODO: send request, create receiver - todo!() + /// Whether or not the track is still published. + pub fn is_published(&self) -> bool { + self.handle().is_published() } - pub fn subscribe_with_target(&self, target_fps: u32) -> DataTrackResult { - todo!() + /// Unpublish the track. + pub fn unpublish(self) { + self.handle().unpublish() } } -pub struct DataTrackSubscription; - -impl Stream for DataTrackSubscription { - type Item = DataTrackFrame; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { - todo!(); - } -} +// TODO: implement remote track (subscriber) \ No newline at end of file From 2028c9159470580e402d3b56e0e7641960308983 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:08:58 +1100 Subject: [PATCH 013/232] Revert older changes --- livekit/src/room/data_track/mod.rs | 16 -------------- livekit/src/room/e2ee/manager.rs | 16 +++++++++----- livekit/src/rtc_engine/rtc_session.rs | 32 ++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 24 deletions(-) delete mode 100644 livekit/src/room/data_track/mod.rs diff --git a/livekit/src/room/data_track/mod.rs b/livekit/src/room/data_track/mod.rs deleted file mode 100644 index ec8941443..000000000 --- a/livekit/src/room/data_track/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2025 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. - -// Re-export APIs from the data track crate. -pub use livekit_datatrack::api::*; \ No newline at end of file diff --git a/livekit/src/room/e2ee/manager.rs b/livekit/src/room/e2ee/manager.rs index 1e583b9c4..65f1aa253 100644 --- a/livekit/src/room/e2ee/manager.rs +++ b/livekit/src/room/e2ee/manager.rs @@ -25,11 +25,7 @@ use parking_lot::Mutex; use super::{key_provider::KeyProvider, EncryptionType}; use crate::{ - e2ee::E2eeOptions, - id::{ParticipantIdentity, TrackSid}, - participant::{LocalParticipant, RemoteParticipant}, - prelude::{LocalTrack, LocalTrackPublication, RemoteTrack, RemoteTrackPublication}, - rtc_engine::lk_runtime::LkRuntime, + data_track, e2ee::E2eeOptions, id::{ParticipantIdentity, TrackSid}, participant::{LocalParticipant, RemoteParticipant}, prelude::{LocalTrack, LocalTrackPublication, RemoteTrack, RemoteTrackPublication}, rtc_engine::lk_runtime::LkRuntime }; type StateChangedHandler = Box; @@ -283,3 +279,13 @@ impl E2eeManager { data_packet_cryptor.encrypt(participant_identity, key_index, data) } } + +impl livekit_datatrack::e2ee::E2eeProvider for E2eeManager { + fn encrypt(&self, payload: bytes::Bytes) -> livekit_datatrack::e2ee::EncryptedPayload { + todo!() + } + + fn decrypt(&self, payload: livekit_datatrack::e2ee::EncryptedPayload) -> bytes::Bytes { + todo!() + } +} \ No newline at end of file diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 950662b57..033ae577f 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -63,6 +63,7 @@ pub const ICE_CONNECT_TIMEOUT: Duration = Duration::from_secs(15); pub const TRACK_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); pub const LOSSY_DC_LABEL: &str = "_lossy"; pub const RELIABLE_DC_LABEL: &str = "_reliable"; +pub const DATA_TRACK_DC_LABEL: &str = "_datatrack"; pub const RELIABLE_RECEIVED_STATE_TTL: Duration = Duration::from_secs(30); pub const PUBLISHER_NEGOTIATION_FREQUENCY: Duration = Duration::from_millis(150); pub const INITIAL_BUFFERED_AMOUNT_LOW_THRESHOLD: u64 = 2 * 1024 * 1024; @@ -117,7 +118,7 @@ pub enum SessionEvent { }, DataTrackPublishedResponse { request_id: u32, - result: proto::data_track_published_response::Result + result: proto::data_track_published_response::Result, }, ChatMessage { participant_identity: ParticipantIdentity, @@ -371,6 +372,9 @@ struct SessionInner { pending_requests: Mutex>>, e2ee_manager: Option, + + data_track_sub: livekit_datatrack::SubManager, + data_track_pub: livekit_datatrack::PubManager, } /// Information about the local participant needed for outgoing @@ -461,6 +465,16 @@ impl RtcSession { DataChannelInit { ordered: true, ..DataChannelInit::default() }, )?; + let mut data_track_dc = publisher_pc.peer_connection().create_data_channel( + DATA_TRACK_DC_LABEL, + DataChannelInit { + ordered: false, + max_retransmits: Some(0), + ..DataChannelInit::default() + }, + )?; + data_track_dc.on_state_change(callback); + // Forward events received inside the signaling thread to our rtc channel rtc_events::forward_pc_events(&mut publisher_pc, rtc_emitter.clone()); rtc_events::forward_pc_events(&mut subscriber_pc, rtc_emitter.clone()); @@ -469,6 +483,16 @@ impl RtcSession { let (close_tx, close_rx) = watch::channel(false); + // let data_track_sub = + // livekit_datatrack::SubManager::new(livekit_datatrack::SubManagerOptions { + // e2ee_provider: None, + // }); + + // let data_track_pub = + // livekit_datatrack::PubManager::new(livekit_datatrack::PubManagerOptions { + // e2ee_provider: None, + // }); + let inner = Arc::new(SessionInner { has_published: Default::default(), fast_publish: AtomicBool::new(join_response.fast_publish), @@ -497,6 +521,8 @@ impl RtcSession { negotiation_queue: NegotiationQueue::new(), pending_requests: Default::default(), e2ee_manager, + data_track_sub, + data_track_pub, }); // Start session tasks @@ -990,7 +1016,7 @@ impl SessionInner { let Some(result) = publish_res.result else { return Ok(()) }; let _ = self.emitter.send(SessionEvent::DataTrackPublishedResponse { request_id: publish_res.request_id, - result + result, }); } proto::signal_response::Message::TrackPublished(publish_res) => { @@ -1297,7 +1323,7 @@ impl SessionInner { req: proto::AddDataTrackRequest, ) -> EngineResult { // TODO: verify unique name before sending request - self.signal_client.send(proto::signal_request::Message::AddDataTrack(req)).await; + // self.signal_client.send(proto::signal_request::Message::AddDataTrack(req)).await; // TODO: await response From 525ec96a93bac37f37e57095f72afdb4f62033ed Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:28:13 +1100 Subject: [PATCH 014/232] Generate lockfile --- Cargo.lock | 752 +++++++++++++++++++++++++++-------------------------- 1 file changed, 390 insertions(+), 362 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c73d0aaf7..f1f66eddd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,9 +82,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -96,7 +96,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "libc", ] @@ -118,7 +118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.9.4", + "bitflags 2.10.0", "cc", "cesu8", "jni", @@ -197,22 +197,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -409,7 +409,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -545,7 +545,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -556,7 +556,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -588,7 +588,7 @@ checksum = "d3ca019570363e800b05ad4fd890734f28ac7b72f563ad8a35079efb793616f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -599,11 +599,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -676,7 +676,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -693,9 +693,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "bzip2" @@ -723,7 +723,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "log", "polling 3.11.0", "rustix 0.38.44", @@ -731,18 +731,43 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "calloop" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" +dependencies = [ + "bitflags 2.10.0", + "polling 3.11.0", + "rustix 1.1.2", + "slab", + "tracing", +] + [[package]] name = "calloop-wayland-source" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ - "calloop", + "calloop 0.13.0", "rustix 0.38.44", "wayland-backend", "wayland-client", ] +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.3", + "rustix 1.1.2", + "wayland-backend", + "wayland-client", +] + [[package]] name = "castaway" version = "0.1.2" @@ -751,9 +776,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.2.41" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ "find-msvc-tools", "jobserver", @@ -778,9 +803,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -824,9 +849,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.49" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -834,9 +859,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.49" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -853,7 +878,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -892,6 +917,17 @@ dependencies = [ "unicode-width 0.2.2", ] +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width 0.2.2", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -1128,9 +1164,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -1153,9 +1189,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.83+curl-8.15.0" +version = "0.4.84+curl-8.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5830daf304027db10c82632a464879d46a3f7c4ba17a31592657ad16c719b483" +checksum = "abc4294dc41b882eaff37973c2ec3ae203d0091341ee68fbadd1d06e0c18a73b" dependencies = [ "cc", "libc", @@ -1174,9 +1210,9 @@ checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" [[package]] name = "cxx" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9c4fe7f2f5dc5c62871a1b43992d197da6fa1394656a94276ac2894a90a6fe" +checksum = "47ac4eaf7ebe29e92f1b091ceefec7710a53a6f6154b2460afda626c113b65b9" dependencies = [ "cc", "cxx-build", @@ -1189,50 +1225,49 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5cf2909d37d80633ddd208676fc27c2608a7f035fff69c882421168038b26dd" +checksum = "2abd4c3021eefbac5149f994c117b426852bca3a0aad227698527bca6d4ea657" dependencies = [ "cc", - "codespan-reporting 0.12.0", - "indexmap 2.11.4", + "codespan-reporting 0.13.1", + "indexmap 2.12.0", "proc-macro2", "quote", "scratch", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "cxxbridge-cmd" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "077f5ee3d3bfd8d27f83208fdaa96ddd50af7f096c77077cc4b94da10bfacefd" +checksum = "6f12fbc5888b2311f23e52a601e11ad7790d8f0dbb903ec26e2513bf5373ed70" dependencies = [ "clap", - "codespan-reporting 0.12.0", - "indexmap 2.11.4", + "codespan-reporting 0.13.1", + "indexmap 2.12.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "cxxbridge-flags" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0108748615125b9f2e915dfafdffcbdabbca9b15102834f6d7e9a768f2f2864" +checksum = "83d3dd7870af06e283f3f8ce0418019c96171c9ce122cfb9c8879de3d84388fd" [[package]] name = "cxxbridge-macro" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e896681ef9b8dc462cfa6961d61909704bde0984b30bcb4082fe102b478890" +checksum = "a26f0d82da663316786791c3d0e9f9edc7d1ee1f04bdad3d2643086a69d6256c" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "proc-macro2", "quote", - "rustversion", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1270,46 +1305,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core", - "quote", - "syn 1.0.109", -] - [[package]] name = "dashmap" version = "5.5.3" @@ -1317,7 +1312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -1337,9 +1332,9 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -1367,7 +1362,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", ] @@ -1379,7 +1374,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1393,9 +1388,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -1468,7 +1463,7 @@ checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" dependencies = [ "accesskit", "ahash", - "bitflags 2.9.4", + "bitflags 2.10.0", "emath", "epaint", "log", @@ -1563,7 +1558,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1678,9 +1673,9 @@ dependencies = [ [[package]] name = "exr" -version = "1.73.0" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" dependencies = [ "bit_field", "half", @@ -1723,7 +1718,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1737,9 +1732,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fixedbitset" @@ -1755,9 +1750,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -1808,7 +1803,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1947,7 +1942,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1982,9 +1977,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -2099,7 +2094,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "gpu-alloc-types", ] @@ -2109,7 +2104,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -2130,7 +2125,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -2141,7 +2136,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -2156,7 +2151,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.11.4", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -2250,11 +2245,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2363,9 +2358,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -2389,15 +2384,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", - "rustls 0.23.32", - "rustls-native-certs 0.8.1", + "rustls 0.23.35", + "rustls-native-certs 0.8.2", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] @@ -2420,7 +2415,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "native-tls", "tokio", @@ -2430,9 +2425,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64 0.22.1", "bytes", @@ -2441,7 +2436,7 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.7.0", + "hyper 1.8.1", "ipnet", "libc", "percent-encoding", @@ -2478,9 +2473,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -2491,9 +2486,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -2504,11 +2499,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -2519,42 +2513,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -2640,9 +2630,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", @@ -2674,9 +2664,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -2684,20 +2674,20 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "isahc" @@ -2781,26 +2771,26 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2846,9 +2836,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -2933,7 +2923,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "redox_syscall 0.5.18", ] @@ -2963,9 +2953,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "libc", @@ -2996,15 +2986,15 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "livekit" @@ -3216,9 +3206,9 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] @@ -3229,7 +3219,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -3262,13 +3252,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "wasi", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3286,9 +3276,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" +checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" dependencies = [ "num-traits", "pxfm", @@ -3308,11 +3298,11 @@ checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg_aliases", "codespan-reporting 0.11.1", "hexf-parse", - "indexmap 2.11.4", + "indexmap 2.12.0", "log", "rustc-hash 1.1.0", "spirv", @@ -3330,13 +3320,13 @@ checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg_aliases", "codespan-reporting 0.12.0", "half", "hashbrown 0.15.5", "hexf-parse", - "indexmap 2.11.4", + "indexmap 2.12.0", "log", "num-traits", "once_cell", @@ -3370,7 +3360,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys 0.5.0+25.2.9519653", @@ -3384,7 +3374,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys 0.6.0+11769913", @@ -3456,7 +3446,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3471,9 +3461,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -3481,14 +3471,14 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3531,7 +3521,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "libc", "objc2 0.5.2", @@ -3547,7 +3537,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-graphics", "objc2-foundation 0.3.2", @@ -3559,7 +3549,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-core-location", @@ -3583,7 +3573,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3595,7 +3585,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dispatch2", "objc2 0.6.3", ] @@ -3606,7 +3596,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dispatch2", "objc2 0.6.3", "objc2-core-foundation", @@ -3649,7 +3639,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "dispatch", "libc", @@ -3662,7 +3652,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-foundation", ] @@ -3673,7 +3663,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-foundation", ] @@ -3696,7 +3686,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3708,7 +3698,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3731,7 +3721,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-cloud-kit", @@ -3763,7 +3753,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-core-location", @@ -3810,17 +3800,17 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -3837,7 +3827,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3848,18 +3838,18 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.5.3+3.5.4" +version = "300.5.4+3.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6bad8cd0233b63971e232cc9c5e83039375b8586d2312f31fda85db8f888c2" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -3870,9 +3860,9 @@ dependencies = [ [[package]] name = "orbclient" -version = "0.3.48" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +checksum = "247ad146e19b9437f8604c21f8652423595cf710ad108af40e77d3ae6e96b827" dependencies = [ "libredox", ] @@ -4005,7 +3995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset 0.4.2", - "indexmap 2.11.4", + "indexmap 2.12.0", ] [[package]] @@ -4015,7 +4005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset 0.5.7", - "indexmap 2.11.4", + "indexmap 2.12.0", ] [[package]] @@ -4035,7 +4025,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -4097,7 +4087,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "crc32fast", "fdeflate", "flate2", @@ -4157,9 +4147,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -4192,7 +4182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -4206,9 +4196,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -4266,7 +4256,7 @@ dependencies = [ "prost 0.12.6", "prost-types 0.12.6", "regex", - "syn 2.0.106", + "syn 2.0.110", "tempfile", ] @@ -4286,7 +4276,7 @@ dependencies = [ "prost 0.14.1", "prost-types 0.14.1", "regex", - "syn 2.0.106", + "syn 2.0.110", "tempfile", ] @@ -4313,7 +4303,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -4326,7 +4316,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -4401,7 +4391,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.32", + "rustls 0.23.35", "socket2 0.6.1", "thiserror 2.0.17", "tokio", @@ -4421,7 +4411,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls 0.23.32", + "rustls 0.23.35", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -4446,9 +4436,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -4565,7 +4555,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -4617,7 +4607,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-rustls", "hyper-tls", "hyper-util", @@ -4627,8 +4617,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.32", - "rustls-native-certs 0.8.1", + "rustls 0.23.35", + "rustls-native-certs 0.8.2", "rustls-pki-types", "serde", "serde_json", @@ -4644,7 +4634,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] @@ -4668,7 +4658,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.9.4", + "bitflags 2.10.0", "serde", "serde_derive", ] @@ -4710,7 +4700,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4723,7 +4713,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -4744,14 +4734,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.7", + "rustls-webpki 0.103.8", "subtle", "zeroize", ] @@ -4770,9 +4760,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -4791,9 +4781,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", "zeroize", @@ -4811,9 +4801,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -4897,7 +4887,7 @@ dependencies = [ "ab_glyph", "log", "memmap2", - "smithay-client-toolkit", + "smithay-client-toolkit 0.19.2", "tiny-skia", ] @@ -4907,7 +4897,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -4920,7 +4910,7 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4983,7 +4973,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -5101,9 +5091,9 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.9.4", - "calloop", - "calloop-wayland-source", + "bitflags 2.10.0", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", "cursor-icon", "libc", "log", @@ -5120,14 +5110,41 @@ dependencies = [ "xkeysym", ] +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.10.0", + "calloop 0.14.3", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.2", + "thiserror 2.0.17", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + [[package]] name = "smithay-clipboard" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" dependencies = [ "libc", - "smithay-client-toolkit", + "smithay-client-toolkit 0.20.0", "wayland-backend", ] @@ -5174,7 +5191,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -5226,7 +5243,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -5248,9 +5265,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -5280,7 +5297,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -5324,7 +5341,7 @@ checksum = "451b374529930d7601b1eef8d32bc79ae870b6079b069401709c2a8bf9e75f36" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -5353,7 +5370,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -5364,7 +5381,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -5447,9 +5464,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -5506,7 +5523,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -5535,7 +5552,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.32", + "rustls 0.23.35", "tokio", ] @@ -5570,9 +5587,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -5596,7 +5613,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "toml_datetime", "toml_parser", "winnow", @@ -5680,7 +5697,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "futures-util", "http 1.3.1", @@ -5724,7 +5741,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -5846,9 +5863,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -5976,9 +5993,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -5987,25 +6004,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -6016,9 +6019,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6026,22 +6029,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.110", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -6066,7 +6069,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "rustix 1.1.2", "wayland-backend", "wayland-scanner", @@ -6078,7 +6081,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cursor-icon", "wayland-backend", ] @@ -6100,9 +6103,35 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfe33d551eb8bffd03ff067a8b44bb963919157841a99957151299a6307d19c" +dependencies = [ + "bitflags 2.10.0", "wayland-backend", "wayland-client", + "wayland-protocols", "wayland-scanner", ] @@ -6112,7 +6141,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6125,7 +6154,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6157,9 +6186,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -6177,9 +6206,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" +checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" dependencies = [ "core-foundation 0.10.1", "jni", @@ -6208,9 +6237,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -6244,9 +6273,9 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "wgpu" @@ -6255,7 +6284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353" dependencies = [ "arrayvec", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg_aliases", "document-features", "js-sys", @@ -6281,7 +6310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" dependencies = [ "arrayvec", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg_aliases", "document-features", "hashbrown 0.15.5", @@ -6310,10 +6339,10 @@ checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499" dependencies = [ "arrayvec", "bit-vec", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg_aliases", "document-features", - "indexmap 2.11.4", + "indexmap 2.12.0", "log", "naga 24.0.0", "once_cell", @@ -6336,11 +6365,11 @@ dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg_aliases", "document-features", "hashbrown 0.15.5", - "indexmap 2.11.4", + "indexmap 2.12.0", "log", "naga 25.0.1", "once_cell", @@ -6394,7 +6423,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "bytemuck", "cfg_aliases", @@ -6437,7 +6466,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "bytemuck", "cfg-if", @@ -6480,7 +6509,7 @@ version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "js-sys", "log", "web-sys", @@ -6492,7 +6521,7 @@ version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "js-sys", "log", @@ -6617,7 +6646,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -6628,7 +6657,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -6639,7 +6668,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -6650,7 +6679,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -6747,14 +6776,14 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] @@ -7011,10 +7040,10 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "bytemuck", - "calloop", + "calloop 0.13.0", "cfg_aliases", "concurrent-queue", "core-foundation 0.9.4", @@ -7036,7 +7065,7 @@ dependencies = [ "redox_syscall 0.4.1", "rustix 0.38.44", "sctk-adwaita", - "smithay-client-toolkit", + "smithay-client-toolkit 0.19.2", "smol_str", "tracing", "unicode-segmentation", @@ -7071,9 +7100,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "x11-dl" @@ -7119,7 +7148,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dlib", "log", "once_cell", @@ -7134,17 +7163,16 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -7152,13 +7180,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "synstructure", ] @@ -7189,7 +7217,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -7209,7 +7237,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "synstructure", ] @@ -7221,9 +7249,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -7232,9 +7260,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -7243,13 +7271,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] From c32bbcf573b7d42e1b4de88577326127f42b1dad Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:40:03 +1100 Subject: [PATCH 015/232] Remove older changes --- Cargo.lock | 1 - livekit/Cargo.toml | 1 - livekit/src/room/e2ee/manager.rs | 16 ++---- livekit/src/room/mod.rs | 12 ++-- .../src/room/participant/local_participant.rs | 14 ----- livekit/src/rtc_engine/mod.rs | 8 --- livekit/src/rtc_engine/rtc_session.rs | 57 ------------------- 7 files changed, 10 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1f66eddd..a087a9da5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3009,7 +3009,6 @@ dependencies = [ "libloading", "libwebrtc", "livekit-api", - "livekit-datatrack", "livekit-protocol", "livekit-runtime", "log", diff --git a/livekit/Cargo.toml b/livekit/Cargo.toml index b0d246fce..b65bf3990 100644 --- a/livekit/Cargo.toml +++ b/livekit/Cargo.toml @@ -28,7 +28,6 @@ __lk-e2e-test = [] # end-to-end testing with a LiveKit server [dependencies] livekit-runtime = { workspace = true } livekit-api = { workspace = true } -livekit-datatrack = { workspace = true } libwebrtc = { workspace = true } livekit-protocol = { workspace = true } prost = "0.12" diff --git a/livekit/src/room/e2ee/manager.rs b/livekit/src/room/e2ee/manager.rs index 65f1aa253..1e583b9c4 100644 --- a/livekit/src/room/e2ee/manager.rs +++ b/livekit/src/room/e2ee/manager.rs @@ -25,7 +25,11 @@ use parking_lot::Mutex; use super::{key_provider::KeyProvider, EncryptionType}; use crate::{ - data_track, e2ee::E2eeOptions, id::{ParticipantIdentity, TrackSid}, participant::{LocalParticipant, RemoteParticipant}, prelude::{LocalTrack, LocalTrackPublication, RemoteTrack, RemoteTrackPublication}, rtc_engine::lk_runtime::LkRuntime + e2ee::E2eeOptions, + id::{ParticipantIdentity, TrackSid}, + participant::{LocalParticipant, RemoteParticipant}, + prelude::{LocalTrack, LocalTrackPublication, RemoteTrack, RemoteTrackPublication}, + rtc_engine::lk_runtime::LkRuntime, }; type StateChangedHandler = Box; @@ -279,13 +283,3 @@ impl E2eeManager { data_packet_cryptor.encrypt(participant_identity, key_index, data) } } - -impl livekit_datatrack::e2ee::E2eeProvider for E2eeManager { - fn encrypt(&self, payload: bytes::Bytes) -> livekit_datatrack::e2ee::EncryptedPayload { - todo!() - } - - fn decrypt(&self, payload: livekit_datatrack::e2ee::EncryptedPayload) -> bytes::Bytes { - todo!() - } -} \ No newline at end of file diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index 9d9f935df..aebe35a56 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -45,10 +45,13 @@ pub use self::{ }; pub use crate::rtc_engine::SimulateScenario; use crate::{ - data_track::DataTrack, participant::ConnectionQuality, prelude::*, registered_audio_filter_plugins, rtc_engine::{ + participant::ConnectionQuality, + prelude::*, + registered_audio_filter_plugins, + rtc_engine::{ EngineError, EngineEvent, EngineEvents, EngineOptions, EngineResult, RtcEngine, SessionStats, INITIAL_BUFFERED_AMOUNT_LOW_THRESHOLD, - } + }, }; pub mod data_stream; @@ -58,7 +61,6 @@ pub mod options; pub mod participant; pub mod publication; pub mod track; -pub mod data_track; pub(crate) mod utils; pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -86,10 +88,6 @@ pub enum RoomError { pub enum RoomEvent { ParticipantConnected(RemoteParticipant), ParticipantDisconnected(RemoteParticipant), - // DataTrackPublished { - // track: DataTrack, - // participant: RemoteParticipant - // }, LocalTrackPublished { publication: LocalTrackPublication, track: LocalTrack, diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 19ca5dd0e..a9a2320fd 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -28,7 +28,6 @@ use crate::{ ByteStreamInfo, ByteStreamWriter, StreamByteOptions, StreamResult, StreamTextOptions, TextStreamInfo, TextStreamWriter, }, - data_track::{self, DataTrack, DataTrackOptions, Local}, e2ee::EncryptionType, options::{self, compute_video_encodings, video_layers_from_encodings, TrackPublishOptions}, prelude::*, @@ -228,19 +227,6 @@ impl LocalParticipant { vec } - pub async fn publish_data_track( - &self, - options: DataTrackOptions, - ) -> RoomResult> { - - // TODO: set use_e2ee based on participant info - let req = options.into_add_track_request(false); - let track_info = self.inner.rtc_engine.add_data_track(req).await?; - - todo!() - - } - pub async fn publish_track( &self, track: LocalTrack, diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index 2cd5143cc..cc8da7ce2 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -267,14 +267,6 @@ impl RtcEngine { session.simulate_scenario(scenario).await } - pub async fn add_data_track(&self, req: proto::AddDataTrackRequest) -> EngineResult { - let (session, _r_lock) = { - let (handle, _r_lock) = self.inner.wait_reconnection().await?; - (handle.session.clone(), _r_lock) - }; - session.add_data_track(req).await - } - pub async fn add_track(&self, req: proto::AddTrackRequest) -> EngineResult { let (session, _r_lock) = { let (handle, _r_lock) = self.inner.wait_reconnection().await?; diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 033ae577f..d3dcb9056 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -63,7 +63,6 @@ pub const ICE_CONNECT_TIMEOUT: Duration = Duration::from_secs(15); pub const TRACK_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); pub const LOSSY_DC_LABEL: &str = "_lossy"; pub const RELIABLE_DC_LABEL: &str = "_reliable"; -pub const DATA_TRACK_DC_LABEL: &str = "_datatrack"; pub const RELIABLE_RECEIVED_STATE_TTL: Duration = Duration::from_secs(30); pub const PUBLISHER_NEGOTIATION_FREQUENCY: Duration = Duration::from_millis(150); pub const INITIAL_BUFFERED_AMOUNT_LOW_THRESHOLD: u64 = 2 * 1024 * 1024; @@ -116,10 +115,6 @@ pub enum SessionEvent { kind: DataPacketKind, encryption_type: proto::encryption::Type, }, - DataTrackPublishedResponse { - request_id: u32, - result: proto::data_track_published_response::Result, - }, ChatMessage { participant_identity: ParticipantIdentity, message: ChatMessage, @@ -372,9 +367,6 @@ struct SessionInner { pending_requests: Mutex>>, e2ee_manager: Option, - - data_track_sub: livekit_datatrack::SubManager, - data_track_pub: livekit_datatrack::PubManager, } /// Information about the local participant needed for outgoing @@ -465,16 +457,6 @@ impl RtcSession { DataChannelInit { ordered: true, ..DataChannelInit::default() }, )?; - let mut data_track_dc = publisher_pc.peer_connection().create_data_channel( - DATA_TRACK_DC_LABEL, - DataChannelInit { - ordered: false, - max_retransmits: Some(0), - ..DataChannelInit::default() - }, - )?; - data_track_dc.on_state_change(callback); - // Forward events received inside the signaling thread to our rtc channel rtc_events::forward_pc_events(&mut publisher_pc, rtc_emitter.clone()); rtc_events::forward_pc_events(&mut subscriber_pc, rtc_emitter.clone()); @@ -483,16 +465,6 @@ impl RtcSession { let (close_tx, close_rx) = watch::channel(false); - // let data_track_sub = - // livekit_datatrack::SubManager::new(livekit_datatrack::SubManagerOptions { - // e2ee_provider: None, - // }); - - // let data_track_pub = - // livekit_datatrack::PubManager::new(livekit_datatrack::PubManagerOptions { - // e2ee_provider: None, - // }); - let inner = Arc::new(SessionInner { has_published: Default::default(), fast_publish: AtomicBool::new(join_response.fast_publish), @@ -521,8 +493,6 @@ impl RtcSession { negotiation_queue: NegotiationQueue::new(), pending_requests: Default::default(), e2ee_manager, - data_track_sub, - data_track_pub, }); // Start session tasks @@ -549,13 +519,6 @@ impl RtcSession { self.inner.publisher_negotiation_needed() } - pub async fn add_data_track( - &self, - req: proto::AddDataTrackRequest, - ) -> EngineResult { - self.inner.add_data_track(req).await - } - pub async fn add_track(&self, req: proto::AddTrackRequest) -> EngineResult { self.inner.add_track(req).await } @@ -1011,14 +974,6 @@ impl SessionInner { let _ = self.emitter.send(SessionEvent::ConnectionQuality { updates: quality.updates }); } - proto::signal_response::Message::DataTrackPublished(publish_res) => { - // TODO: Why is this optional? - let Some(result) = publish_res.result else { return Ok(()) }; - let _ = self.emitter.send(SessionEvent::DataTrackPublishedResponse { - request_id: publish_res.request_id, - result, - }); - } proto::signal_response::Message::TrackPublished(publish_res) => { let mut pending_tracks = self.pending_tracks.lock(); if let Some(tx) = pending_tracks.remove(&publish_res.cid) { @@ -1318,18 +1273,6 @@ impl SessionInner { } } - async fn add_data_track( - &self, - req: proto::AddDataTrackRequest, - ) -> EngineResult { - // TODO: verify unique name before sending request - // self.signal_client.send(proto::signal_request::Message::AddDataTrack(req)).await; - - // TODO: await response - - todo!() - } - async fn add_track(&self, req: proto::AddTrackRequest) -> EngineResult { let (tx, rx) = oneshot::channel(); let cid = req.cid.clone(); From 971c72bd6d363a83f3440c48d492313895ad5536 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:50:46 +1100 Subject: [PATCH 016/232] Fix protocol integration --- livekit-ffi/protocol/participant.proto | 1 + livekit-ffi/src/conversion/participant.rs | 1 + livekit/src/proto.rs | 1 + livekit/src/room/mod.rs | 2 ++ livekit/src/room/participant/mod.rs | 1 + livekit/src/rtc_engine/rtc_session.rs | 2 ++ 6 files changed, 8 insertions(+) diff --git a/livekit-ffi/protocol/participant.proto b/livekit-ffi/protocol/participant.proto index 390deaa9c..b74ed3709 100644 --- a/livekit-ffi/protocol/participant.proto +++ b/livekit-ffi/protocol/participant.proto @@ -40,6 +40,7 @@ enum ParticipantKind { PARTICIPANT_KIND_EGRESS = 2; PARTICIPANT_KIND_SIP = 3; PARTICIPANT_KIND_AGENT = 4; + PARTICIPANT_KIND_CONNECTOR = 5; } enum DisconnectReason { diff --git a/livekit-ffi/src/conversion/participant.rs b/livekit-ffi/src/conversion/participant.rs index 7a56dc2ff..04a07e788 100644 --- a/livekit-ffi/src/conversion/participant.rs +++ b/livekit-ffi/src/conversion/participant.rs @@ -46,6 +46,7 @@ impl From for proto::ParticipantKind { ParticipantKind::Ingress => proto::ParticipantKind::Ingress, ParticipantKind::Egress => proto::ParticipantKind::Egress, ParticipantKind::Agent => proto::ParticipantKind::Agent, + ParticipantKind::Connector => proto::ParticipantKind::Connector } } } diff --git a/livekit/src/proto.rs b/livekit/src/proto.rs index ee178b040..3d791be8d 100644 --- a/livekit/src/proto.rs +++ b/livekit/src/proto.rs @@ -154,6 +154,7 @@ impl From for participant::ParticipantKind { participant_info::Kind::Egress => participant::ParticipantKind::Egress, participant_info::Kind::Sip => participant::ParticipantKind::Sip, participant_info::Kind::Agent => participant::ParticipantKind::Agent, + participant_info::Kind::Connector => participant::ParticipantKind::Connector } } } diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index aebe35a56..b99194c16 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -1186,11 +1186,13 @@ impl RoomSession { sdp: answer.to_string(), r#type: answer.sdp_type().to_string(), id: 0, + mid_to_track_id: HashMap::default(), }), offer: Some(proto::SessionDescription { sdp: offer.to_string(), r#type: offer.sdp_type().to_string(), id: 0, + mid_to_track_id: HashMap::default(), }), track_sids_disabled: Vec::default(), // TODO: New protocol version subscription: Some(proto::UpdateSubscription { diff --git a/livekit/src/room/participant/mod.rs b/livekit/src/room/participant/mod.rs index cbe687776..a1a019bea 100644 --- a/livekit/src/room/participant/mod.rs +++ b/livekit/src/room/participant/mod.rs @@ -44,6 +44,7 @@ pub enum ParticipantKind { Egress, Sip, Agent, + Connector } #[derive(Debug, Clone, Copy, Eq, PartialEq)] diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index d3dcb9056..780ffb6ce 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -932,6 +932,7 @@ impl SessionInner { r#type: "answer".to_string(), sdp: answer.to_string(), id: 0, + mid_to_track_id: HashMap::default(), })) .await; } @@ -1056,6 +1057,7 @@ impl SessionInner { r#type: "offer".to_string(), sdp: offer.to_string(), id: 0, + mid_to_track_id: HashMap::default(), })) .await; } From b962e5ba5c8d2f27907ed64caf908ca39212ab94 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:23:52 +1100 Subject: [PATCH 017/232] Make modules public --- livekit-datatrack/src/lib.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index 5dae100c2..47e0d9761 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -13,9 +13,7 @@ // limitations under the License. mod dtp; -mod frame; -mod track; -mod error; -mod manager; - -pub use manager::*; \ No newline at end of file +pub mod error; +pub mod frame; +pub mod manager; +pub mod track; \ No newline at end of file From 737539c6644f1ef189b2bfec140959817a5cf9bb Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:26:41 +1100 Subject: [PATCH 018/232] Rename struct --- livekit-datatrack/src/manager/publish.rs | 8 ++++---- livekit-datatrack/src/track.rs | 10 ++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/livekit-datatrack/src/manager/publish.rs b/livekit-datatrack/src/manager/publish.rs index a588976b6..7edfc07fa 100644 --- a/livekit-datatrack/src/manager/publish.rs +++ b/livekit-datatrack/src/manager/publish.rs @@ -17,7 +17,7 @@ use crate::{ error::{InternalError, PublishError, PublishFrameError, PublishFrameErrorReason}, frame::DataTrackFrame, manager::e2ee::EncryptionProvider, - track::{DataTrack, DataTrackInfo, Local, PublishOptions, TrackHandle}, + track::{DataTrack, DataTrackInfo, Local, DataTrackOptions, TrackHandle}, }; use anyhow::{anyhow, Context}; use bytes::Bytes; @@ -166,7 +166,7 @@ impl PubManager { /// Publishes a data track with the given options. pub async fn publish_track( &self, - options: PublishOptions, + options: DataTrackOptions, ) -> Result, PublishError> { let (result_tx, result_rx) = oneshot::channel(); let request = PubRequest { options, result_tx }; @@ -317,7 +317,7 @@ impl PubManagerTask { } struct PubRequest { - options: PublishOptions, + options: DataTrackOptions, result_tx: oneshot::Sender, PublishError>>, } @@ -342,7 +342,7 @@ pub enum PubSignalInput { RequestResponse(proto::RequestResponse), } -impl PublishOptions { +impl DataTrackOptions { fn into_add_track_request( self, use_e2ee: bool, diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index 65bf1e8d9..83adeb246 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -24,12 +24,12 @@ pub use crate::dtp::TrackHandle; /// Options for publishing a data track. #[derive(Clone, Debug)] -pub struct PublishOptions { +pub struct DataTrackOptions { pub(crate) name: String, pub(crate) disable_e2ee: bool, } -impl PublishOptions { +impl DataTrackOptions { pub fn with_name(name: impl Into) -> Self { Self { name: name.into(), disable_e2ee: false } } @@ -66,6 +66,12 @@ pub struct Local; #[derive(Debug)] pub struct Remote; +/// A data track published by the local participant. +pub type LocalDataTrack = DataTrack; + +/// A data track published by a remote participant. +pub type RemoteDataTrack = DataTrack; + #[derive(Debug, Clone)] pub struct DataTrack { info: Arc, From 8016280397a296f4410252403f3954c84439c01a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:39:18 +1100 Subject: [PATCH 019/232] Create data track example --- Cargo.lock | 10 +++++++ Cargo.toml | 1 + examples/data_track/Cargo.toml | 18 +++++++++++++ examples/data_track/src/publisher.rs | 38 +++++++++++++++++++++++++++ examples/data_track/src/subscriber.rs | 6 +++++ 5 files changed, 73 insertions(+) create mode 100644 examples/data_track/Cargo.toml create mode 100644 examples/data_track/src/publisher.rs create mode 100644 examples/data_track/src/subscriber.rs diff --git a/Cargo.lock b/Cargo.lock index a087a9da5..d86eea137 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1330,6 +1330,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "data_track" +version = "0.1.0" +dependencies = [ + "anyhow", + "livekit", + "tokio", +] + [[package]] name = "deranged" version = "0.5.5" @@ -3009,6 +3018,7 @@ dependencies = [ "libloading", "libwebrtc", "livekit-api", + "livekit-datatrack", "livekit-protocol", "livekit-runtime", "log", diff --git a/Cargo.toml b/Cargo.toml index e644eeaa6..06dfe28b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "examples/api", "examples/basic_room", "examples/basic_text_stream", + "examples/data_track", "examples/encrypted_text_stream", "examples/local_audio", "examples/mobile", diff --git a/examples/data_track/Cargo.toml b/examples/data_track/Cargo.toml new file mode 100644 index 000000000..2cbb400ec --- /dev/null +++ b/examples/data_track/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "data_track" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +tokio = { version = "1", features = ["full"] } +livekit = { workspace = true, features = ["rustls-tls-native-roots"] } +anyhow = "1.0.100" + +[[bin]] +name = "publisher" +path = "src/publisher.rs" + +[[bin]] +name = "subscriber" +path = "src/subscriber.rs" diff --git a/examples/data_track/src/publisher.rs b/examples/data_track/src/publisher.rs new file mode 100644 index 000000000..ec1326286 --- /dev/null +++ b/examples/data_track/src/publisher.rs @@ -0,0 +1,38 @@ +use anyhow::Result; +use livekit::{ + data_track::{DataTrack, DataTrackFrameBuilder, DataTrackOptions, Local}, + prelude::*, +}; +use std::{ + env, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; +use tokio::{signal, time}; + +#[tokio::main] +async fn main() -> Result<()> { + let url = env::var("LIVEKIT_URL").expect("LIVEKIT_URL is not set"); + let token = env::var("LIVEKIT_TOKEN").expect("LIVEKIT_TOKEN is not set"); + + let (room, rx) = Room::connect(&url, &token, RoomOptions::default()).await?; + + let options = DataTrackOptions::with_name("brightness"); + let track = room.local_participant().publish_data_track(options).await?; + + tokio::select! { + _ = publish_frames(track) => {} + _ = signal::ctrl_c() => {} + } + Ok(()) +} + +async fn publish_frames(track: DataTrack) { + loop { + let frame = DataTrackFrameBuilder::new(vec![0xFA; 256]); + track + .publish(frame.build()) + .inspect_err(|err| println!("Failed to publish frame: {}", err)) + .ok(); + time::sleep(Duration::from_millis(500)).await + } +} diff --git a/examples/data_track/src/subscriber.rs b/examples/data_track/src/subscriber.rs new file mode 100644 index 000000000..fde05e1a0 --- /dev/null +++ b/examples/data_track/src/subscriber.rs @@ -0,0 +1,6 @@ +use anyhow::Result; + +#[tokio::main] +async fn main() -> Result<()> { + panic!("Not implemented") +} \ No newline at end of file From 2f0c0cce633249f178fd8d93e1a4dd6e9764f6ca Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:41:14 +1100 Subject: [PATCH 020/232] Add publish method --- livekit/Cargo.toml | 1 + livekit/src/room/data_track.rs | 18 ++++++++++++++++++ livekit/src/room/mod.rs | 3 +++ .../src/room/participant/local_participant.rs | 8 ++++++++ 4 files changed, 30 insertions(+) create mode 100644 livekit/src/room/data_track.rs diff --git a/livekit/Cargo.toml b/livekit/Cargo.toml index b65bf3990..6b7d9fa59 100644 --- a/livekit/Cargo.toml +++ b/livekit/Cargo.toml @@ -30,6 +30,7 @@ livekit-runtime = { workspace = true } livekit-api = { workspace = true } libwebrtc = { workspace = true } livekit-protocol = { workspace = true } +livekit-datatrack = { workspace = true } prost = "0.12" serde = { version = "1", features = ["derive"] } serde_json = "1.0" diff --git a/livekit/src/room/data_track.rs b/livekit/src/room/data_track.rs new file mode 100644 index 000000000..3eb413eac --- /dev/null +++ b/livekit/src/room/data_track.rs @@ -0,0 +1,18 @@ +// Copyright 2025 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. + +// Re-export data track API types. + +pub use livekit_datatrack::{error::*, frame::*, track::*}; +pub(crate) use livekit_datatrack::manager::*; diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index b99194c16..4018112e1 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -54,6 +54,7 @@ use crate::{ }, }; +pub mod data_track; pub mod data_stream; pub mod e2ee; pub mod id; @@ -81,6 +82,8 @@ pub enum RoomError { AlreadyClosed, #[error("request error: {reason:?} - {message}")] Request { reason: proto::request_response::Reason, message: String }, + #[error(transparent)] + DataTrack(data_track::PublishError) } #[derive(Clone, Debug)] diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index a9a2320fd..c79a4092b 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -28,6 +28,7 @@ use crate::{ ByteStreamInfo, ByteStreamWriter, StreamByteOptions, StreamResult, StreamTextOptions, TextStreamInfo, TextStreamWriter, }, + data_track::{DataTrackOptions, DataTrack, Local}, e2ee::EncryptionType, options::{self, compute_video_encodings, video_layers_from_encodings, TrackPublishOptions}, prelude::*, @@ -227,6 +228,13 @@ impl LocalParticipant { vec } + pub async fn publish_data_track( + &self, + options: DataTrackOptions, + ) -> RoomResult> { + todo!() + } + pub async fn publish_track( &self, track: LocalTrack, From e345b6c7a0dbbfcfda6410f3fcd669add65a323e Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:49:24 +1100 Subject: [PATCH 021/232] Integrate publisher --- Cargo.lock | 2 + examples/data_track/Cargo.toml | 3 + examples/data_track/src/publisher.rs | 2 + livekit-datatrack/src/manager/e2ee.rs | 28 ++++-- livekit-datatrack/src/manager/mod.rs | 1 + livekit-datatrack/src/manager/publish.rs | 14 ++- livekit/src/room/e2ee/manager.rs | 30 ++++++- livekit/src/room/mod.rs | 4 +- .../src/room/participant/local_participant.rs | 17 ++-- livekit/src/rtc_engine/mod.rs | 16 +++- livekit/src/rtc_engine/rtc_session.rs | 90 ++++++++++++++++++- 11 files changed, 179 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d86eea137..80d50abb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1335,7 +1335,9 @@ name = "data_track" version = "0.1.0" dependencies = [ "anyhow", + "env_logger 0.11.8", "livekit", + "log", "tokio", ] diff --git a/examples/data_track/Cargo.toml b/examples/data_track/Cargo.toml index 2cbb400ec..f49ab8622 100644 --- a/examples/data_track/Cargo.toml +++ b/examples/data_track/Cargo.toml @@ -8,6 +8,9 @@ publish = false tokio = { version = "1", features = ["full"] } livekit = { workspace = true, features = ["rustls-tls-native-roots"] } anyhow = "1.0.100" +log = "0.4.26" +env_logger = "0.11.7" + [[bin]] name = "publisher" diff --git a/examples/data_track/src/publisher.rs b/examples/data_track/src/publisher.rs index ec1326286..706779a89 100644 --- a/examples/data_track/src/publisher.rs +++ b/examples/data_track/src/publisher.rs @@ -11,6 +11,8 @@ use tokio::{signal, time}; #[tokio::main] async fn main() -> Result<()> { + env_logger::init(); + let url = env::var("LIVEKIT_URL").expect("LIVEKIT_URL is not set"); let token = env::var("LIVEKIT_TOKEN").expect("LIVEKIT_TOKEN is not set"); diff --git a/livekit-datatrack/src/manager/e2ee.rs b/livekit-datatrack/src/manager/e2ee.rs index a605db6fe..50225802a 100644 --- a/livekit-datatrack/src/manager/e2ee.rs +++ b/livekit-datatrack/src/manager/e2ee.rs @@ -12,19 +12,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fmt::Debug; use bytes::Bytes; +use std::fmt::Debug; +use thiserror::Error; -pub struct EncryptedPayload { +pub struct EncryptedPayload<'a> { pub payload: Bytes, - pub iv: [u8; 12], - pub key_index: u8 + pub iv: &'a [u8; 12], + pub key_index: u8, } -pub trait EncryptionProvider: Debug { - fn encrypt(&self, payload: Bytes) -> EncryptedPayload; +#[derive(Debug, Error)] +#[error("Encryption failed")] +pub struct EncryptionError; + +pub trait EncryptionProvider: Send + Sync + Debug { + /// Encrypt the given payload. + fn encrypt(&self, payload: Bytes) -> Result; } -pub trait DecryptionProvider: Debug { - fn decrypt(&self, payload: EncryptedPayload) -> Bytes; +#[derive(Debug, Error)] +#[error("Decryption failed")] +pub struct DecryptionError; + +pub trait DecryptionProvider: Send + Sync + Debug { + /// Decrypt the given payload. + fn decrypt(&self, payload: EncryptedPayload) -> Result; + // TODO: handle publisher identity } \ No newline at end of file diff --git a/livekit-datatrack/src/manager/mod.rs b/livekit-datatrack/src/manager/mod.rs index 503b39d60..13fab6b18 100644 --- a/livekit-datatrack/src/manager/mod.rs +++ b/livekit-datatrack/src/manager/mod.rs @@ -16,5 +16,6 @@ pub mod e2ee; pub mod publish; // pub mod subscribe; +pub use e2ee::*; pub use publish::*; // pub use subscribe::*; \ No newline at end of file diff --git a/livekit-datatrack/src/manager/publish.rs b/livekit-datatrack/src/manager/publish.rs index 7edfc07fa..a4f7a2970 100644 --- a/livekit-datatrack/src/manager/publish.rs +++ b/livekit-datatrack/src/manager/publish.rs @@ -17,7 +17,7 @@ use crate::{ error::{InternalError, PublishError, PublishFrameError, PublishFrameErrorReason}, frame::DataTrackFrame, manager::e2ee::EncryptionProvider, - track::{DataTrack, DataTrackInfo, Local, DataTrackOptions, TrackHandle}, + track::{DataTrack, DataTrackInfo, DataTrackOptions, Local, TrackHandle}, }; use anyhow::{anyhow, Context}; use bytes::Bytes; @@ -74,6 +74,7 @@ impl Drop for PubHandle { struct TrackPubTask { // TODO: packetizer, e2ee_provider, rate tracking, etc. + encryption: Option>, info: Arc, state_rx: watch::Receiver, frame_rx: mpsc::Receiver, @@ -253,6 +254,7 @@ impl PubManagerTask { let task = TrackPubTask { // TODO: handle cancellation + encryption: self.encryption.clone(), info: info.clone(), frame_rx, state_rx, @@ -395,3 +397,13 @@ impl TryInto for proto::DataTrackInfo { Ok(DataTrackInfo { handle, sid: self.sid, name: self.name, uses_e2ee }) } } + +impl Into for PubSignalOutput { + fn into(self) -> proto::signal_request::Message { + use proto::signal_request::Message; + match self { + PubSignalOutput::PublishRequest(req) => Message::PublishDataTrack(req), + PubSignalOutput::UnpublishRequest(req) => Message::UnpublishDataTrack(req) + } + } +} diff --git a/livekit/src/room/e2ee/manager.rs b/livekit/src/room/e2ee/manager.rs index 1e583b9c4..2057a4453 100644 --- a/livekit/src/room/e2ee/manager.rs +++ b/livekit/src/room/e2ee/manager.rs @@ -25,12 +25,14 @@ use parking_lot::Mutex; use super::{key_provider::KeyProvider, EncryptionType}; use crate::{ + data_track, e2ee::E2eeOptions, id::{ParticipantIdentity, TrackSid}, participant::{LocalParticipant, RemoteParticipant}, prelude::{LocalTrack, LocalTrackPublication, RemoteTrack, RemoteTrackPublication}, rtc_engine::lk_runtime::LkRuntime, }; +use std::fmt::Debug; type StateChangedHandler = Box; @@ -269,7 +271,7 @@ impl E2eeManager { } /// Encrypt data for transmission over a data channel - pub fn encrypt_data( + pub(crate) fn encrypt_data( &self, data: &[u8], participant_identity: &str, @@ -283,3 +285,29 @@ impl E2eeManager { data_packet_cryptor.encrypt(participant_identity, key_index, data) } } + +impl Debug for E2eeManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("E2eeManager") + .field("enabled", &self.inner.lock().enabled) + .finish_non_exhaustive() + } +} + +impl data_track::DecryptionProvider for E2eeManager { + fn decrypt( + &self, + payload: data_track::EncryptedPayload, + ) -> Result { + todo!() + } +} + +impl data_track::EncryptionProvider for E2eeManager { + fn encrypt( + &self, + payload: bytes::Bytes, + ) -> Result { + todo!() + } +} diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index 4018112e1..d3ccffd2a 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -81,9 +81,7 @@ pub enum RoomError { #[error("already closed")] AlreadyClosed, #[error("request error: {reason:?} - {message}")] - Request { reason: proto::request_response::Reason, message: String }, - #[error(transparent)] - DataTrack(data_track::PublishError) + Request { reason: proto::request_response::Reason, message: String } } #[derive(Clone, Debug)] diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index c79a4092b..21b66cc93 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -24,17 +24,10 @@ use std::{ use super::{ConnectionQuality, ParticipantInner, ParticipantKind, ParticipantTrackPermission}; use crate::{ - data_stream::{ + ChatMessage, DataPacket, RoomSession, RpcAck, RpcRequest, RpcResponse, SipDTMF, Transcription, data_stream::{ ByteStreamInfo, ByteStreamWriter, StreamByteOptions, StreamResult, StreamTextOptions, TextStreamInfo, TextStreamWriter, - }, - data_track::{DataTrackOptions, DataTrack, Local}, - e2ee::EncryptionType, - options::{self, compute_video_encodings, video_layers_from_encodings, TrackPublishOptions}, - prelude::*, - room::participant::rpc::{RpcError, RpcErrorCode, RpcInvocationData, MAX_PAYLOAD_BYTES}, - rtc_engine::{EngineError, RtcEngine}, - ChatMessage, DataPacket, RoomSession, RpcAck, RpcRequest, RpcResponse, SipDTMF, Transcription, + }, data_track::{self, DataTrack, DataTrackOptions, Local}, e2ee::EncryptionType, options::{self, TrackPublishOptions, compute_video_encodings, video_layers_from_encodings}, prelude::*, room::participant::rpc::{MAX_PAYLOAD_BYTES, RpcError, RpcErrorCode, RpcInvocationData}, rtc_engine::{EngineError, RtcEngine} }; use chrono::Utc; use libwebrtc::{native::create_random_uuid, rtp_parameters::RtpEncodingParameters}; @@ -228,13 +221,15 @@ impl LocalParticipant { vec } + /// Publish a data track. pub async fn publish_data_track( &self, options: DataTrackOptions, - ) -> RoomResult> { - todo!() + ) -> Result, data_track::PublishError> { + self.inner.rtc_engine.publish_data_track(options).await } + /// Publish a media track. pub async fn publish_track( &self, track: LocalTrack, diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index cc8da7ce2..a3ef5f184 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -16,6 +16,8 @@ use std::{borrow::Cow, fmt::Debug, sync::Arc, time::Duration}; use libwebrtc::prelude::*; use livekit_api::signal_client::{SignalError, SignalOptions}; +use livekit_datatrack::error::PublishError; +use livekit_datatrack::track::{DataTrack, DataTrackOptions, Local}; use livekit_protocol as proto; use livekit_runtime::{interval, Interval, JoinHandle}; use parking_lot::{RwLock, RwLockReadGuard}; @@ -27,6 +29,7 @@ use tokio::sync::{ pub use self::rtc_session::{SessionStats, INITIAL_BUFFERED_AMOUNT_LOW_THRESHOLD}; use crate::prelude::ParticipantIdentity; +use crate::{data_track, ChatMessage, E2eeManager, TranscriptionSegment}; use crate::{ id::ParticipantSid, options::TrackPublishOptions, @@ -38,7 +41,6 @@ use crate::{ }, DataPacketKind, }; -use crate::{ChatMessage, E2eeManager, TranscriptionSegment}; pub mod lk_runtime; mod peer_transport; @@ -267,6 +269,18 @@ impl RtcEngine { session.simulate_scenario(scenario).await } + pub async fn publish_data_track( + &self, + options: DataTrackOptions, + ) -> Result, PublishError> { + let (session, _r_lock) = { + let (handle, _r_lock) = + self.inner.wait_reconnection().await.map_err(|_| PublishError::Timeout)?; + (handle.session.clone(), _r_lock) + }; + session.publish_data_track(options).await + } + pub async fn add_track(&self, req: proto::AddTrackRequest) -> EngineResult { let (session, _r_lock) = { let (handle, _r_lock) = self.inner.wait_reconnection().await?; diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 780ffb6ce..cff94a595 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -23,8 +23,11 @@ use std::{ time::Duration, }; +use bytes::Bytes; +use futures_util::{Stream, StreamExt}; use libwebrtc::{prelude::*, stats::RtcStats}; use livekit_api::signal_client::{SignalClient, SignalEvent, SignalEvents}; +use livekit_datatrack::{error::PublishError, track::{DataTrack, DataTrackOptions, Local}}; use livekit_protocol::{self as proto}; use livekit_runtime::{sleep, JoinHandle}; use parking_lot::Mutex; @@ -38,6 +41,7 @@ use tokio::sync::{mpsc, oneshot, watch, Notify}; use super::{rtc_events, EngineError, EngineOptions, EngineResult, SimulateScenario}; use crate::{ + data_track, id::ParticipantIdentity, utils::{ ttl_map::TtlMap, @@ -63,6 +67,7 @@ pub const ICE_CONNECT_TIMEOUT: Duration = Duration::from_secs(15); pub const TRACK_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); pub const LOSSY_DC_LABEL: &str = "_lossy"; pub const RELIABLE_DC_LABEL: &str = "_reliable"; +pub const DATA_TRACK_DC_LABEL: &str = "_datatrack"; pub const RELIABLE_RECEIVED_STATE_TTL: Duration = Duration::from_secs(30); pub const PUBLISHER_NEGOTIATION_FREQUENCY: Duration = Duration::from_millis(150); pub const INITIAL_BUFFERED_AMOUNT_LOW_THRESHOLD: u64 = 2 * 1024 * 1024; @@ -341,6 +346,7 @@ struct SessionInner { lossy_dc_buffered_amount_low_threshold: AtomicU64, reliable_dc: DataChannel, reliable_dc_buffered_amount_low_threshold: AtomicU64, + dt_transport: DataChannel, /// Next sequence number for reliable packets. next_packet_sequence: AtomicU32, @@ -367,6 +373,8 @@ struct SessionInner { pending_requests: Mutex>>, e2ee_manager: Option, + + dt_pub_manager: data_track::PubManager, } /// Information about the local participant needed for outgoing @@ -407,6 +415,8 @@ struct SessionHandle { signal_task: JoinHandle<()>, rtc_task: JoinHandle<()>, dc_task: JoinHandle<()>, + dt_forward_task: JoinHandle<()>, + dt_pub_task: JoinHandle>, } impl RtcSession { @@ -457,12 +467,31 @@ impl RtcSession { DataChannelInit { ordered: true, ..DataChannelInit::default() }, )?; + let dt_transport = publisher_pc.peer_connection().create_data_channel( + DATA_TRACK_DC_LABEL, + DataChannelInit { + ordered: false, + max_retransmits: Some(0), + ..DataChannelInit::default() + }, + )?; + // Forward events received inside the signaling thread to our rtc channel rtc_events::forward_pc_events(&mut publisher_pc, rtc_emitter.clone()); rtc_events::forward_pc_events(&mut subscriber_pc, rtc_emitter.clone()); rtc_events::forward_dc_events(&mut lossy_dc, DataPacketKind::Lossy, rtc_emitter.clone()); rtc_events::forward_dc_events(&mut reliable_dc, DataPacketKind::Reliable, rtc_emitter); + let dt_pub_options = data_track::PubManagerOptions { + encryption: e2ee_manager + .clone() + .filter(|m| m.enabled()) + .map(|m| Arc::new(m) as Arc), + }; + + let (dt_pub_manager, dt_pub_task, dt_pub_signal_out, dt_pub_packet_out) = + data_track::PubManager::new(dt_pub_options); + let (close_tx, close_rx) = watch::channel(false); let inner = Arc::new(SessionInner { @@ -480,6 +509,7 @@ impl RtcSession { reliable_dc_buffered_amount_low_threshold: AtomicU64::new( INITIAL_BUFFERED_AMOUNT_LOW_THRESHOLD, ), + dt_transport, next_packet_sequence: 1.into(), packet_rx_state: Mutex::new(TtlMap::new(RELIABLE_RECEIVED_STATE_TTL)), participant_info, @@ -493,6 +523,7 @@ impl RtcSession { negotiation_queue: NegotiationQueue::new(), pending_requests: Default::default(), e2ee_manager, + dt_pub_manager, }); // Start session tasks @@ -500,9 +531,23 @@ impl RtcSession { livekit_runtime::spawn(inner.clone().signal_task(signal_events, close_rx.clone())); let rtc_task = livekit_runtime::spawn(inner.clone().rtc_session_task(rtc_events, close_rx.clone())); - let dc_task = livekit_runtime::spawn(inner.clone().data_channel_task(dc_events, close_rx)); - - let handle = Mutex::new(Some(SessionHandle { close_tx, signal_task, rtc_task, dc_task })); + let dc_task = + livekit_runtime::spawn(inner.clone().data_channel_task(dc_events, close_rx.clone())); + let dt_forward_task = livekit_runtime::spawn(inner.clone().data_track_forward_task( + dt_pub_signal_out, + dt_pub_packet_out, + close_rx, + )); + let dt_pub_task = livekit_runtime::spawn(dt_pub_task.run()); + + let handle = Mutex::new(Some(SessionHandle { + close_tx, + signal_task, + rtc_task, + dc_task, + dt_forward_task, + dt_pub_task, + })); Ok((Self { inner, handle }, join_response, session_events)) } @@ -545,6 +590,7 @@ impl RtcSession { let _ = handle.rtc_task.await; let _ = handle.signal_task.await; let _ = handle.dc_task.await; + let _ = handle.dt_pub_task.await.inspect_err(|err| log::error!("{}", err)); } // Close the PeerConnections after the task @@ -552,6 +598,13 @@ impl RtcSession { self.inner.close().await; } + pub async fn publish_data_track( + &self, + options: DataTrackOptions, + ) -> Result, PublishError> { + self.inner.publish_data_track(options).await + } + pub async fn publish_data( &self, data: proto::DataPacket, @@ -736,6 +789,28 @@ impl SessionInner { log::debug!("closing signal_task"); } + async fn data_track_forward_task( + self: Arc, + signal_out: impl Stream, + packet_out: impl Stream, + mut close_rx: watch::Receiver, + ) { + tokio::pin!(signal_out); + tokio::pin!(packet_out); + loop { + tokio::select! { + Some(message) = signal_out.next() => { + self.signal_client.send(message.into()).await + }, + Some(packet) = packet_out.next() => { + _ = self.dt_transport.send(&packet, true) + }, + _ = close_rx.changed() => break + } + } + log::debug!("closing data_track_transport_task"); + } + async fn data_channel_task( self: Arc, mut dc_events: mpsc::UnboundedReceiver, @@ -1475,6 +1550,13 @@ impl SessionInner { Ok(()) } + pub async fn publish_data_track( + &self, + options: DataTrackOptions, + ) -> Result, PublishError> { + self.dt_pub_manager.publish_track(options).await + } + async fn publish_data( self: &Arc, mut packet: proto::DataPacket, @@ -1803,6 +1885,8 @@ impl SessionInner { } } +struct DataTrackTransportTask {} + macro_rules! make_rtc_config { ($fncname:ident, $proto:ty) => { fn $fncname(value: $proto, mut config: RtcConfiguration) -> RtcConfiguration { From 1d7bc2d1f365b45fc72e922e425a5cedc45ba02f Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:07:19 +1100 Subject: [PATCH 022/232] Bug fixes --- livekit-datatrack/src/manager/publish.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/livekit-datatrack/src/manager/publish.rs b/livekit-datatrack/src/manager/publish.rs index a4f7a2970..2910850ef 100644 --- a/livekit-datatrack/src/manager/publish.rs +++ b/livekit-datatrack/src/manager/publish.rs @@ -171,13 +171,15 @@ impl PubManager { ) -> Result, PublishError> { let (result_tx, result_rx) = oneshot::channel(); let request = PubRequest { options, result_tx }; - self.pub_req_tx.send(request); + self.pub_req_tx + .try_send(request) + .map_err(|_| PublishError::Disconnected)?; // TODO: move timeout inside pub manager let track = timeout(Self::PUBLISH_TIMEOUT, result_rx) .await .map_err(|_| PublishError::Timeout)? - .map_err(|_| PublishError::Internal(anyhow!("Request rx closed").into()))??; + .map_err(|_| PublishError::Disconnected)??; Ok(track) } } @@ -212,11 +214,11 @@ impl PubManagerTask { fn handle_publish_req(&mut self, req: PubRequest) -> Result<(), InternalError> { let Some(handle) = self.handle_allocator.get() else { - req.result_tx.send(Err(PublishError::LimitReached)); + _ = req.result_tx.send(Err(PublishError::LimitReached)); return Ok(()); }; - if !self.pending_publications.insert(handle, req.result_tx).is_some() { + if !self.pending_publications.insert(handle, req.result_tx).is_none() { Err(anyhow!("Publication already pending for handle"))? } From 64d84aa9b1e3420b6b11310e0f2b5ff5b13300d7 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:01:58 +1100 Subject: [PATCH 023/232] Add TODO --- livekit-datatrack/src/frame.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/livekit-datatrack/src/frame.rs b/livekit-datatrack/src/frame.rs index 5d837e92f..fdc4c3349 100644 --- a/livekit-datatrack/src/frame.rs +++ b/livekit-datatrack/src/frame.rs @@ -52,3 +52,5 @@ impl DataTrackFrameBuilder { } } } + +// TODO: just show payload length in debug. \ No newline at end of file From 2ee77251455e101530a6dae7296f22bf753eacd5 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:02:31 +1100 Subject: [PATCH 024/232] Add error reason --- livekit-datatrack/src/error.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/livekit-datatrack/src/error.rs b/livekit-datatrack/src/error.rs index 63ddd7cb3..414b4d751 100644 --- a/livekit-datatrack/src/error.rs +++ b/livekit-datatrack/src/error.rs @@ -26,6 +26,8 @@ pub enum PublishError { Timeout, #[error("No more data tracks are able to be published")] LimitReached, + #[error("Cannot publish data track when disconnected")] + Disconnected, #[error(transparent)] Internal(#[from] InternalError), } From 9118fe5400243b427abbf0d5f9f60cfdcfc9f168 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 15 Nov 2025 20:22:27 +1100 Subject: [PATCH 025/232] Create packetizer --- livekit-datatrack/src/dtp/mod.rs | 2 ++ livekit-datatrack/src/dtp/packetizer.rs | 44 ++++++++++++++++++++++++ livekit-datatrack/src/manager/e2ee.rs | 4 +-- livekit-datatrack/src/manager/publish.rs | 41 ++++++++++++++++------ 4 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 livekit-datatrack/src/dtp/packetizer.rs diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index 3fc75bc32..65aeff7b2 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -13,11 +13,13 @@ // limitations under the License. mod packet; +mod packetizer; mod track_handle; mod deserialize; mod serialize; pub use packet::*; +pub use packetizer::*; pub use track_handle::*; pub use deserialize::*; pub use serialize::*; \ No newline at end of file diff --git a/livekit-datatrack/src/dtp/packetizer.rs b/livekit-datatrack/src/dtp/packetizer.rs new file mode 100644 index 000000000..dbb13ea66 --- /dev/null +++ b/livekit-datatrack/src/dtp/packetizer.rs @@ -0,0 +1,44 @@ +// Copyright 2025 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 crate::dtp::{Dtp, E2ee, TrackHandle}; +use bytes::Bytes; + +/// Converts application-level frames into packets for transport. +pub struct Packetizer { + track_handle: TrackHandle, + mtu_size: usize, + sequence: u16, + frame_number: u16, + timestamp: u32, +} + +/// Application-level frame packetized by [`Packetizer`]. +pub struct PacketizerFrame { + pub payload: Bytes, + pub e2ee: Option, + pub user_timestamp: Option, +} + +impl Packetizer { + /// Creates a new packetizer. + pub fn new(track_handle: TrackHandle, mtu_size: usize) -> Self { + Self { mtu_size, track_handle, sequence: 0, frame_number: 0, timestamp: 0 } + } + + /// Packetizes a frame into one or more packets. + pub fn packetize(&mut self, frame: PacketizerFrame) -> impl IntoIterator { + vec![] // TODO: + } +} \ No newline at end of file diff --git a/livekit-datatrack/src/manager/e2ee.rs b/livekit-datatrack/src/manager/e2ee.rs index 50225802a..a700c7fe0 100644 --- a/livekit-datatrack/src/manager/e2ee.rs +++ b/livekit-datatrack/src/manager/e2ee.rs @@ -16,9 +16,9 @@ use bytes::Bytes; use std::fmt::Debug; use thiserror::Error; -pub struct EncryptedPayload<'a> { +pub struct EncryptedPayload { pub payload: Bytes, - pub iv: &'a [u8; 12], + pub iv: [u8; 12], pub key_index: u8, } diff --git a/livekit-datatrack/src/manager/publish.rs b/livekit-datatrack/src/manager/publish.rs index 2910850ef..08edf1dc1 100644 --- a/livekit-datatrack/src/manager/publish.rs +++ b/livekit-datatrack/src/manager/publish.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::{ - dtp::TrackHandleAllocator, + dtp, error::{InternalError, PublishError, PublishFrameError, PublishFrameErrorReason}, frame::DataTrackFrame, manager::e2ee::EncryptionProvider, @@ -74,6 +74,7 @@ impl Drop for PubHandle { struct TrackPubTask { // TODO: packetizer, e2ee_provider, rate tracking, etc. + packetizer: dtp::Packetizer, encryption: Option>, info: Arc, state_rx: watch::Receiver, @@ -88,7 +89,7 @@ impl TrackPubTask { while matches!(state, DataTrackState::Published) { tokio::select! { _ = self.state_rx.changed() => Ok(state = *self.state_rx.borrow()), - Some(frame) = self.frame_rx.recv() => self.packetize_and_send(frame), + Some(frame) = self.frame_rx.recv() => self.publish_frame(frame), else => break } .inspect_err(|err| log::error!("{}", err)) @@ -102,8 +103,29 @@ impl TrackPubTask { Ok(()) } - fn packetize_and_send(&self, frame: DataTrackFrame) -> Result<(), InternalError> { - todo!() + fn publish_frame(&mut self, mut frame: DataTrackFrame) -> Result<(), InternalError> { + let mut e2ee: Option = None; + if let Some(encryption) = &self.encryption { + debug_assert!(self.info.uses_e2ee); + let encrypted_payload = + encryption.encrypt(frame.payload).context("Failed to encrypt frame")?; + e2ee = Some(dtp::E2ee { + key_index: encrypted_payload.key_index, + iv: encrypted_payload.iv, + }); + frame.payload = encrypted_payload.payload; + } + + let frame = dtp::PacketizerFrame { + payload: frame.payload, + e2ee, + user_timestamp: frame.user_timestamp, + }; + for packet in self.packetizer.packetize(frame) { + let serialized = packet.serialize(); + self.packet_out_tx.try_send(serialized).context("Failed to send packet")?; + } + Ok(()) } fn send_unpublish_req(self) -> Result<(), InternalError> { @@ -144,7 +166,7 @@ impl PubManager { signal_in_rx, signal_out_tx, packet_out_tx, - handle_allocator: TrackHandleAllocator::default(), + handle_allocator: dtp::TrackHandleAllocator::default(), pending_publications: HashMap::new(), active_publications: HashMap::new(), }; @@ -171,9 +193,7 @@ impl PubManager { ) -> Result, PublishError> { let (result_tx, result_rx) = oneshot::channel(); let request = PubRequest { options, result_tx }; - self.pub_req_tx - .try_send(request) - .map_err(|_| PublishError::Disconnected)?; + self.pub_req_tx.try_send(request).map_err(|_| PublishError::Disconnected)?; // TODO: move timeout inside pub manager let track = timeout(Self::PUBLISH_TIMEOUT, result_rx) @@ -190,7 +210,7 @@ pub struct PubManagerTask { signal_in_rx: mpsc::Receiver, signal_out_tx: mpsc::Sender, packet_out_tx: mpsc::Sender, - handle_allocator: TrackHandleAllocator, + handle_allocator: dtp::TrackHandleAllocator, pending_publications: HashMap, PublishError>>>, active_publications: HashMap>, @@ -256,6 +276,7 @@ impl PubManagerTask { let task = TrackPubTask { // TODO: handle cancellation + packetizer: dtp::Packetizer::new(info.handle, 16_000), encryption: self.encryption.clone(), info: info.clone(), frame_rx, @@ -405,7 +426,7 @@ impl Into for PubSignalOutput { use proto::signal_request::Message; match self { PubSignalOutput::PublishRequest(req) => Message::PublishDataTrack(req), - PubSignalOutput::UnpublishRequest(req) => Message::UnpublishDataTrack(req) + PubSignalOutput::UnpublishRequest(req) => Message::UnpublishDataTrack(req), } } } From ff60baafa67bf0c93a0b50ad8e899acd205fe024 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:55:04 +1100 Subject: [PATCH 026/232] Protocol updates --- livekit-datatrack/src/manager/publish.rs | 10 +- livekit-protocol/protocol | 2 +- livekit-protocol/src/livekit.rs | 179 +++++- livekit-protocol/src/livekit.serde.rs | 671 +++++++++++++++++++---- 4 files changed, 735 insertions(+), 127 deletions(-) diff --git a/livekit-datatrack/src/manager/publish.rs b/livekit-datatrack/src/manager/publish.rs index 08edf1dc1..13dc64456 100644 --- a/livekit-datatrack/src/manager/publish.rs +++ b/livekit-datatrack/src/manager/publish.rs @@ -320,7 +320,11 @@ impl PubManagerTask { &mut self, res: proto::UnpublishDataTrackResponse, ) -> Result<(), InternalError> { - let handle: TrackHandle = res.pub_handle.try_into().context("Invalid handle")?; + let handle = { + let info: DataTrackInfo = + res.info.context("Missing info")?.try_into().context("Invalid info")?; + info.handle + }; let Some(state_tx) = self.active_publications.remove(&handle) else { Err(anyhow!("Cannot handle unpublish for unknown track {}", handle))? }; @@ -425,8 +429,8 @@ impl Into for PubSignalOutput { fn into(self) -> proto::signal_request::Message { use proto::signal_request::Message; match self { - PubSignalOutput::PublishRequest(req) => Message::PublishDataTrack(req), - PubSignalOutput::UnpublishRequest(req) => Message::UnpublishDataTrack(req), + PubSignalOutput::PublishRequest(req) => Message::PublishDataTrackRequest(req), + PubSignalOutput::UnpublishRequest(req) => Message::UnpublishDataTrackRequest(req), } } } diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index 5386ca2ff..21f690495 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit 5386ca2ff8bad8f187e2d0e9037e82150e3ae0e4 +Subproject commit 21f690495229eb17470b40015a2d579fb53f7056 diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index 3475b9af3..0a2cf3c5e 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -83,8 +83,11 @@ pub struct EventMetric { pub struct MetricsRecordingHeader { #[prost(string, tag="1")] pub room_id: ::prost::alloc::string::String, - #[prost(bool, optional, tag="2")] - pub enable_user_data_training: ::core::option::Option, + /// milliseconds + #[prost(uint64, tag="3")] + pub duration: u64, + #[prost(message, optional, tag="4")] + pub start_time: ::core::option::Option<::pbjson_types::Timestamp>, } // // Protocol used to record metrics for a specific session. @@ -354,7 +357,6 @@ pub struct ParticipantInfo { pub disconnect_reason: i32, #[prost(enumeration="participant_info::KindDetail", repeated, tag="18")] pub kind_details: ::prost::alloc::vec::Vec, - /// NEXT_ID: 20 #[prost(message, repeated, tag="19")] pub data_tracks: ::prost::alloc::vec::Vec, } @@ -447,6 +449,8 @@ pub mod participant_info { pub enum KindDetail { CloudAgent = 0, Forwarded = 1, + ConnectorWhatsapp = 2, + ConnectorTwilio = 3, } impl KindDetail { /// String value of the enum field names used in the ProtoBuf definition. @@ -457,6 +461,8 @@ pub mod participant_info { match self { KindDetail::CloudAgent => "CLOUD_AGENT", KindDetail::Forwarded => "FORWARDED", + KindDetail::ConnectorWhatsapp => "CONNECTOR_WHATSAPP", + KindDetail::ConnectorTwilio => "CONNECTOR_TWILIO", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -464,6 +470,8 @@ pub mod participant_info { match value { "CLOUD_AGENT" => Some(Self::CloudAgent), "FORWARDED" => Some(Self::Forwarded), + "CONNECTOR_WHATSAPP" => Some(Self::ConnectorWhatsapp), + "CONNECTOR_TWILIO" => Some(Self::ConnectorTwilio), _ => None, } } @@ -605,10 +613,17 @@ pub struct DataTrackInfo { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackExtensionParticipantSid { + #[prost(enumeration="DataTrackExtensionId", tag="1")] + pub id: i32, + #[prost(string, tag="2")] + pub participant_sid: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct DataTrackSubscriptionOptions { /// Rate in frames per second (FPS) the subscriber wants to receive frames at. - /// If omitted, the subscriber defaults to the publisher's nominal FPS; if the - /// publisher has no nominal FPS, it will use the maximum. + /// If omitted, the subscriber defaults to the publisher's fps #[prost(uint32, optional, tag="1")] pub target_fps: ::core::option::Option, } @@ -1700,6 +1715,32 @@ impl TrackSource { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] +pub enum DataTrackExtensionId { + DteiInvalid = 0, + DteiParticipantSid = 1, +} +impl DataTrackExtensionId { + /// 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 { + DataTrackExtensionId::DteiInvalid => "DTEI_INVALID", + DataTrackExtensionId::DteiParticipantSid => "DTEI_PARTICIPANT_SID", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "DTEI_INVALID" => Some(Self::DteiInvalid), + "DTEI_PARTICIPANT_SID" => Some(Self::DteiParticipantSid), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] pub enum VideoQuality { Low = 0, Medium = 1, @@ -2627,6 +2668,10 @@ pub struct StreamInfo { pub status: i32, #[prost(string, tag="6")] pub error: ::prost::alloc::string::String, + #[prost(int64, tag="7")] + pub last_retry_at: i64, + #[prost(uint32, tag="8")] + pub retries: u32, } /// Nested message and enum types in `StreamInfo`. pub mod stream_info { @@ -3120,10 +3165,10 @@ pub mod signal_request { UpdateVideoTrack(super::UpdateLocalVideoTrack), /// Publish a data track #[prost(message, tag="19")] - PublishDataTrack(super::PublishDataTrackRequest), + PublishDataTrackRequest(super::PublishDataTrackRequest), /// Unpublish a data track #[prost(message, tag="20")] - UnpublishDataTrack(super::UnpublishDataTrackRequest), + UnpublishDataTrackRequest(super::UnpublishDataTrackRequest), /// Update subscription state for one or more data tracks #[prost(message, tag="21")] UpdateDataSubscription(super::UpdateDataSubscription), @@ -3220,13 +3265,13 @@ pub mod signal_response { SubscribedAudioCodecUpdate(super::SubscribedAudioCodecUpdate), /// Sent in response to `PublishDataTrackRequest`. #[prost(message, tag="27")] - PublishDataTrack(super::PublishDataTrackResponse), + PublishDataTrackResponse(super::PublishDataTrackResponse), /// Sent in response to `UnpublishDataTrackRequest` or SFU-initiated unpublish. #[prost(message, tag="28")] - UnpublishDataTrack(super::UnpublishDataTrackResponse), + UnpublishDataTrackResponse(super::UnpublishDataTrackResponse), /// Sent to data track subscribers to provide mapping from track SIDs to handles. #[prost(message, tag="29")] - DataTrackHandles(super::DataTrackSubscriberHandles), + DataTrackSubscriberHandles(super::DataTrackSubscriberHandles), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -3323,16 +3368,29 @@ pub struct UnpublishDataTrackRequest { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct UnpublishDataTrackResponse { - /// Publisher handle of the track that was unpublished. - #[prost(uint32, tag="1")] - pub pub_handle: u32, + /// Information about the unpublished track. + #[prost(message, optional, tag="1")] + pub info: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DataTrackSubscriberHandles { /// Maps handles from incoming packets to the track SIDs that the packets belong to. - #[prost(map="uint32, string", tag="1")] - pub sub_handles: ::std::collections::HashMap, + #[prost(map="uint32, message", tag="1")] + pub sub_handles: ::std::collections::HashMap, +} +/// Nested message and enum types in `DataTrackSubscriberHandles`. +pub mod data_track_subscriber_handles { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] + pub struct PublishedDataTrack { + #[prost(string, tag="1")] + pub publisher_identity: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub publisher_sid: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub track_sid: ::prost::alloc::string::String, + } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3461,14 +3519,12 @@ pub mod update_data_subscription { #[derive(Clone, PartialEq, ::prost::Message)] pub struct Update { #[prost(string, tag="1")] - pub sid: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub participant_identity: ::prost::alloc::string::String, - #[prost(bool, tag="3")] + pub track_sid: ::prost::alloc::string::String, + #[prost(bool, tag="2")] pub subscribe: bool, /// Options to apply when initially subscribing or updating an existing subscription. /// When unsubscribing, this field is ignored. - #[prost(message, optional, tag="4")] + #[prost(message, optional, tag="3")] pub options: ::core::option::Option, } } @@ -3750,6 +3806,8 @@ pub struct SyncState { pub track_sids_disabled: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(message, repeated, tag="7")] pub datachannel_receive_states: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="8")] + pub publish_data_tracks: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -6116,11 +6174,15 @@ pub enum SipStatusCode { SipStatusCallIsForwarded = 181, SipStatusQueued = 182, SipStatusSessionProgress = 183, + SipStatusEarlyDialogTerminated = 199, SipStatusOk = 200, SipStatusAccepted = 202, + SipStatusNoNotification = 204, + SipStatusMultipleChoices = 300, SipStatusMovedPermanently = 301, SipStatusMovedTemporarily = 302, SipStatusUseProxy = 305, + SipStatusAlternativeService = 380, SipStatusBadRequest = 400, SipStatusUnauthorized = 401, SipStatusPaymentRequired = 402, @@ -6132,13 +6194,30 @@ pub enum SipStatusCode { SipStatusRequestTimeout = 408, SipStatusConflict = 409, SipStatusGone = 410, + SipStatusLengthRequired = 411, + SipStatusConditionalRequestFailed = 412, SipStatusRequestEntityTooLarge = 413, SipStatusRequestUriTooLong = 414, SipStatusUnsupportedMediaType = 415, SipStatusRequestedRangeNotSatisfiable = 416, + SipStatusUnknownResourcePriority = 417, SipStatusBadExtension = 420, SipStatusExtensionRequired = 421, + SipStatusSessionIntervalTooSmall = 422, SipStatusIntervalTooBrief = 423, + SipStatusBadLocationInformation = 424, + SipStatusBadAlertMessage = 425, + SipStatusUseIdentityHeader = 428, + SipStatusProvideReferrerIdentity = 429, + SipStatusFlowFailed = 430, + SipStatusAnonymityDisallowed = 433, + SipStatusBadIdentityInfo = 436, + SipStatusUnsupportedCertificate = 437, + SipStatusInvalidIdentityHeader = 438, + SipStatusFirstHopLacksOutboundSupport = 439, + SipStatusMaxBreadthExceeded = 440, + SipStatusBadInfoPackage = 469, + SipStatusConsentNeeded = 470, SipStatusTemporarilyUnavailable = 480, SipStatusCallTransactionDoesNotExists = 481, SipStatusLoopDetected = 482, @@ -6148,6 +6227,10 @@ pub enum SipStatusCode { SipStatusBusyHere = 486, SipStatusRequestTerminated = 487, SipStatusNotAcceptableHere = 488, + SipStatusBadEvent = 489, + SipStatusRequestPending = 491, + SipStatusUndecipherable = 493, + SipStatusSecurityAgreementRequired = 494, SipStatusInternalServerError = 500, SipStatusNotImplemented = 501, SipStatusBadGateway = 502, @@ -6159,6 +6242,8 @@ pub enum SipStatusCode { SipStatusGlobalDecline = 603, SipStatusGlobalDoesNotExistAnywhere = 604, SipStatusGlobalNotAcceptable = 606, + SipStatusGlobalUnwanted = 607, + SipStatusGlobalRejected = 608, } impl SipStatusCode { /// String value of the enum field names used in the ProtoBuf definition. @@ -6173,11 +6258,15 @@ impl SipStatusCode { SipStatusCode::SipStatusCallIsForwarded => "SIP_STATUS_CALL_IS_FORWARDED", SipStatusCode::SipStatusQueued => "SIP_STATUS_QUEUED", SipStatusCode::SipStatusSessionProgress => "SIP_STATUS_SESSION_PROGRESS", + SipStatusCode::SipStatusEarlyDialogTerminated => "SIP_STATUS_EARLY_DIALOG_TERMINATED", SipStatusCode::SipStatusOk => "SIP_STATUS_OK", SipStatusCode::SipStatusAccepted => "SIP_STATUS_ACCEPTED", + SipStatusCode::SipStatusNoNotification => "SIP_STATUS_NO_NOTIFICATION", + SipStatusCode::SipStatusMultipleChoices => "SIP_STATUS_MULTIPLE_CHOICES", SipStatusCode::SipStatusMovedPermanently => "SIP_STATUS_MOVED_PERMANENTLY", SipStatusCode::SipStatusMovedTemporarily => "SIP_STATUS_MOVED_TEMPORARILY", SipStatusCode::SipStatusUseProxy => "SIP_STATUS_USE_PROXY", + SipStatusCode::SipStatusAlternativeService => "SIP_STATUS_ALTERNATIVE_SERVICE", SipStatusCode::SipStatusBadRequest => "SIP_STATUS_BAD_REQUEST", SipStatusCode::SipStatusUnauthorized => "SIP_STATUS_UNAUTHORIZED", SipStatusCode::SipStatusPaymentRequired => "SIP_STATUS_PAYMENT_REQUIRED", @@ -6189,13 +6278,30 @@ impl SipStatusCode { SipStatusCode::SipStatusRequestTimeout => "SIP_STATUS_REQUEST_TIMEOUT", SipStatusCode::SipStatusConflict => "SIP_STATUS_CONFLICT", SipStatusCode::SipStatusGone => "SIP_STATUS_GONE", + SipStatusCode::SipStatusLengthRequired => "SIP_STATUS_LENGTH_REQUIRED", + SipStatusCode::SipStatusConditionalRequestFailed => "SIP_STATUS_CONDITIONAL_REQUEST_FAILED", SipStatusCode::SipStatusRequestEntityTooLarge => "SIP_STATUS_REQUEST_ENTITY_TOO_LARGE", SipStatusCode::SipStatusRequestUriTooLong => "SIP_STATUS_REQUEST_URI_TOO_LONG", SipStatusCode::SipStatusUnsupportedMediaType => "SIP_STATUS_UNSUPPORTED_MEDIA_TYPE", SipStatusCode::SipStatusRequestedRangeNotSatisfiable => "SIP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE", + SipStatusCode::SipStatusUnknownResourcePriority => "SIP_STATUS_UNKNOWN_RESOURCE_PRIORITY", SipStatusCode::SipStatusBadExtension => "SIP_STATUS_BAD_EXTENSION", SipStatusCode::SipStatusExtensionRequired => "SIP_STATUS_EXTENSION_REQUIRED", + SipStatusCode::SipStatusSessionIntervalTooSmall => "SIP_STATUS_SESSION_INTERVAL_TOO_SMALL", SipStatusCode::SipStatusIntervalTooBrief => "SIP_STATUS_INTERVAL_TOO_BRIEF", + SipStatusCode::SipStatusBadLocationInformation => "SIP_STATUS_BAD_LOCATION_INFORMATION", + SipStatusCode::SipStatusBadAlertMessage => "SIP_STATUS_BAD_ALERT_MESSAGE", + SipStatusCode::SipStatusUseIdentityHeader => "SIP_STATUS_USE_IDENTITY_HEADER", + SipStatusCode::SipStatusProvideReferrerIdentity => "SIP_STATUS_PROVIDE_REFERRER_IDENTITY", + SipStatusCode::SipStatusFlowFailed => "SIP_STATUS_FLOW_FAILED", + SipStatusCode::SipStatusAnonymityDisallowed => "SIP_STATUS_ANONYMITY_DISALLOWED", + SipStatusCode::SipStatusBadIdentityInfo => "SIP_STATUS_BAD_IDENTITY_INFO", + SipStatusCode::SipStatusUnsupportedCertificate => "SIP_STATUS_UNSUPPORTED_CERTIFICATE", + SipStatusCode::SipStatusInvalidIdentityHeader => "SIP_STATUS_INVALID_IDENTITY_HEADER", + SipStatusCode::SipStatusFirstHopLacksOutboundSupport => "SIP_STATUS_FIRST_HOP_LACKS_OUTBOUND_SUPPORT", + SipStatusCode::SipStatusMaxBreadthExceeded => "SIP_STATUS_MAX_BREADTH_EXCEEDED", + SipStatusCode::SipStatusBadInfoPackage => "SIP_STATUS_BAD_INFO_PACKAGE", + SipStatusCode::SipStatusConsentNeeded => "SIP_STATUS_CONSENT_NEEDED", SipStatusCode::SipStatusTemporarilyUnavailable => "SIP_STATUS_TEMPORARILY_UNAVAILABLE", SipStatusCode::SipStatusCallTransactionDoesNotExists => "SIP_STATUS_CALL_TRANSACTION_DOES_NOT_EXISTS", SipStatusCode::SipStatusLoopDetected => "SIP_STATUS_LOOP_DETECTED", @@ -6205,6 +6311,10 @@ impl SipStatusCode { SipStatusCode::SipStatusBusyHere => "SIP_STATUS_BUSY_HERE", SipStatusCode::SipStatusRequestTerminated => "SIP_STATUS_REQUEST_TERMINATED", SipStatusCode::SipStatusNotAcceptableHere => "SIP_STATUS_NOT_ACCEPTABLE_HERE", + SipStatusCode::SipStatusBadEvent => "SIP_STATUS_BAD_EVENT", + SipStatusCode::SipStatusRequestPending => "SIP_STATUS_REQUEST_PENDING", + SipStatusCode::SipStatusUndecipherable => "SIP_STATUS_UNDECIPHERABLE", + SipStatusCode::SipStatusSecurityAgreementRequired => "SIP_STATUS_SECURITY_AGREEMENT_REQUIRED", SipStatusCode::SipStatusInternalServerError => "SIP_STATUS_INTERNAL_SERVER_ERROR", SipStatusCode::SipStatusNotImplemented => "SIP_STATUS_NOT_IMPLEMENTED", SipStatusCode::SipStatusBadGateway => "SIP_STATUS_BAD_GATEWAY", @@ -6216,6 +6326,8 @@ impl SipStatusCode { SipStatusCode::SipStatusGlobalDecline => "SIP_STATUS_GLOBAL_DECLINE", SipStatusCode::SipStatusGlobalDoesNotExistAnywhere => "SIP_STATUS_GLOBAL_DOES_NOT_EXIST_ANYWHERE", SipStatusCode::SipStatusGlobalNotAcceptable => "SIP_STATUS_GLOBAL_NOT_ACCEPTABLE", + SipStatusCode::SipStatusGlobalUnwanted => "SIP_STATUS_GLOBAL_UNWANTED", + SipStatusCode::SipStatusGlobalRejected => "SIP_STATUS_GLOBAL_REJECTED", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -6227,11 +6339,15 @@ impl SipStatusCode { "SIP_STATUS_CALL_IS_FORWARDED" => Some(Self::SipStatusCallIsForwarded), "SIP_STATUS_QUEUED" => Some(Self::SipStatusQueued), "SIP_STATUS_SESSION_PROGRESS" => Some(Self::SipStatusSessionProgress), + "SIP_STATUS_EARLY_DIALOG_TERMINATED" => Some(Self::SipStatusEarlyDialogTerminated), "SIP_STATUS_OK" => Some(Self::SipStatusOk), "SIP_STATUS_ACCEPTED" => Some(Self::SipStatusAccepted), + "SIP_STATUS_NO_NOTIFICATION" => Some(Self::SipStatusNoNotification), + "SIP_STATUS_MULTIPLE_CHOICES" => Some(Self::SipStatusMultipleChoices), "SIP_STATUS_MOVED_PERMANENTLY" => Some(Self::SipStatusMovedPermanently), "SIP_STATUS_MOVED_TEMPORARILY" => Some(Self::SipStatusMovedTemporarily), "SIP_STATUS_USE_PROXY" => Some(Self::SipStatusUseProxy), + "SIP_STATUS_ALTERNATIVE_SERVICE" => Some(Self::SipStatusAlternativeService), "SIP_STATUS_BAD_REQUEST" => Some(Self::SipStatusBadRequest), "SIP_STATUS_UNAUTHORIZED" => Some(Self::SipStatusUnauthorized), "SIP_STATUS_PAYMENT_REQUIRED" => Some(Self::SipStatusPaymentRequired), @@ -6243,13 +6359,30 @@ impl SipStatusCode { "SIP_STATUS_REQUEST_TIMEOUT" => Some(Self::SipStatusRequestTimeout), "SIP_STATUS_CONFLICT" => Some(Self::SipStatusConflict), "SIP_STATUS_GONE" => Some(Self::SipStatusGone), + "SIP_STATUS_LENGTH_REQUIRED" => Some(Self::SipStatusLengthRequired), + "SIP_STATUS_CONDITIONAL_REQUEST_FAILED" => Some(Self::SipStatusConditionalRequestFailed), "SIP_STATUS_REQUEST_ENTITY_TOO_LARGE" => Some(Self::SipStatusRequestEntityTooLarge), "SIP_STATUS_REQUEST_URI_TOO_LONG" => Some(Self::SipStatusRequestUriTooLong), "SIP_STATUS_UNSUPPORTED_MEDIA_TYPE" => Some(Self::SipStatusUnsupportedMediaType), "SIP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE" => Some(Self::SipStatusRequestedRangeNotSatisfiable), + "SIP_STATUS_UNKNOWN_RESOURCE_PRIORITY" => Some(Self::SipStatusUnknownResourcePriority), "SIP_STATUS_BAD_EXTENSION" => Some(Self::SipStatusBadExtension), "SIP_STATUS_EXTENSION_REQUIRED" => Some(Self::SipStatusExtensionRequired), + "SIP_STATUS_SESSION_INTERVAL_TOO_SMALL" => Some(Self::SipStatusSessionIntervalTooSmall), "SIP_STATUS_INTERVAL_TOO_BRIEF" => Some(Self::SipStatusIntervalTooBrief), + "SIP_STATUS_BAD_LOCATION_INFORMATION" => Some(Self::SipStatusBadLocationInformation), + "SIP_STATUS_BAD_ALERT_MESSAGE" => Some(Self::SipStatusBadAlertMessage), + "SIP_STATUS_USE_IDENTITY_HEADER" => Some(Self::SipStatusUseIdentityHeader), + "SIP_STATUS_PROVIDE_REFERRER_IDENTITY" => Some(Self::SipStatusProvideReferrerIdentity), + "SIP_STATUS_FLOW_FAILED" => Some(Self::SipStatusFlowFailed), + "SIP_STATUS_ANONYMITY_DISALLOWED" => Some(Self::SipStatusAnonymityDisallowed), + "SIP_STATUS_BAD_IDENTITY_INFO" => Some(Self::SipStatusBadIdentityInfo), + "SIP_STATUS_UNSUPPORTED_CERTIFICATE" => Some(Self::SipStatusUnsupportedCertificate), + "SIP_STATUS_INVALID_IDENTITY_HEADER" => Some(Self::SipStatusInvalidIdentityHeader), + "SIP_STATUS_FIRST_HOP_LACKS_OUTBOUND_SUPPORT" => Some(Self::SipStatusFirstHopLacksOutboundSupport), + "SIP_STATUS_MAX_BREADTH_EXCEEDED" => Some(Self::SipStatusMaxBreadthExceeded), + "SIP_STATUS_BAD_INFO_PACKAGE" => Some(Self::SipStatusBadInfoPackage), + "SIP_STATUS_CONSENT_NEEDED" => Some(Self::SipStatusConsentNeeded), "SIP_STATUS_TEMPORARILY_UNAVAILABLE" => Some(Self::SipStatusTemporarilyUnavailable), "SIP_STATUS_CALL_TRANSACTION_DOES_NOT_EXISTS" => Some(Self::SipStatusCallTransactionDoesNotExists), "SIP_STATUS_LOOP_DETECTED" => Some(Self::SipStatusLoopDetected), @@ -6259,6 +6392,10 @@ impl SipStatusCode { "SIP_STATUS_BUSY_HERE" => Some(Self::SipStatusBusyHere), "SIP_STATUS_REQUEST_TERMINATED" => Some(Self::SipStatusRequestTerminated), "SIP_STATUS_NOT_ACCEPTABLE_HERE" => Some(Self::SipStatusNotAcceptableHere), + "SIP_STATUS_BAD_EVENT" => Some(Self::SipStatusBadEvent), + "SIP_STATUS_REQUEST_PENDING" => Some(Self::SipStatusRequestPending), + "SIP_STATUS_UNDECIPHERABLE" => Some(Self::SipStatusUndecipherable), + "SIP_STATUS_SECURITY_AGREEMENT_REQUIRED" => Some(Self::SipStatusSecurityAgreementRequired), "SIP_STATUS_INTERNAL_SERVER_ERROR" => Some(Self::SipStatusInternalServerError), "SIP_STATUS_NOT_IMPLEMENTED" => Some(Self::SipStatusNotImplemented), "SIP_STATUS_BAD_GATEWAY" => Some(Self::SipStatusBadGateway), @@ -6270,6 +6407,8 @@ impl SipStatusCode { "SIP_STATUS_GLOBAL_DECLINE" => Some(Self::SipStatusGlobalDecline), "SIP_STATUS_GLOBAL_DOES_NOT_EXIST_ANYWHERE" => Some(Self::SipStatusGlobalDoesNotExistAnywhere), "SIP_STATUS_GLOBAL_NOT_ACCEPTABLE" => Some(Self::SipStatusGlobalNotAcceptable), + "SIP_STATUS_GLOBAL_UNWANTED" => Some(Self::SipStatusGlobalUnwanted), + "SIP_STATUS_GLOBAL_REJECTED" => Some(Self::SipStatusGlobalRejected), _ => None, } } diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index e025dc1f7..91b658418 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -7098,6 +7098,192 @@ impl<'de> serde::Deserialize<'de> for data_stream::Trailer { deserializer.deserialize_struct("livekit.DataStream.Trailer", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for DataTrackExtensionId { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::DteiInvalid => "DTEI_INVALID", + Self::DteiParticipantSid => "DTEI_PARTICIPANT_SID", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for DataTrackExtensionId { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "DTEI_INVALID", + "DTEI_PARTICIPANT_SID", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackExtensionId; + + 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 { + "DTEI_INVALID" => Ok(DataTrackExtensionId::DteiInvalid), + "DTEI_PARTICIPANT_SID" => Ok(DataTrackExtensionId::DteiParticipantSid), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for DataTrackExtensionParticipantSid { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.id != 0 { + len += 1; + } + if !self.participant_sid.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackExtensionParticipantSid", len)?; + if self.id != 0 { + let v = DataTrackExtensionId::try_from(self.id) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.id)))?; + struct_ser.serialize_field("id", &v)?; + } + if !self.participant_sid.is_empty() { + struct_ser.serialize_field("participantSid", &self.participant_sid)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataTrackExtensionParticipantSid { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "id", + "participant_sid", + "participantSid", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Id, + ParticipantSid, + __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 { + "id" => Ok(GeneratedField::Id), + "participantSid" | "participant_sid" => Ok(GeneratedField::ParticipantSid), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackExtensionParticipantSid; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackExtensionParticipantSid") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut id__ = None; + let mut participant_sid__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id__ = Some(map_.next_value::()? as i32); + } + GeneratedField::ParticipantSid => { + if participant_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("participantSid")); + } + participant_sid__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataTrackExtensionParticipantSid { + id: id__.unwrap_or_default(), + participant_sid: participant_sid__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackExtensionParticipantSid", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for DataTrackInfo { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -7348,6 +7534,138 @@ impl<'de> serde::Deserialize<'de> for DataTrackSubscriberHandles { deserializer.deserialize_struct("livekit.DataTrackSubscriberHandles", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for data_track_subscriber_handles::PublishedDataTrack { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.publisher_identity.is_empty() { + len += 1; + } + if !self.publisher_sid.is_empty() { + len += 1; + } + if !self.track_sid.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSubscriberHandles.PublishedDataTrack", len)?; + if !self.publisher_identity.is_empty() { + struct_ser.serialize_field("publisherIdentity", &self.publisher_identity)?; + } + if !self.publisher_sid.is_empty() { + struct_ser.serialize_field("publisherSid", &self.publisher_sid)?; + } + if !self.track_sid.is_empty() { + struct_ser.serialize_field("trackSid", &self.track_sid)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for data_track_subscriber_handles::PublishedDataTrack { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "publisher_identity", + "publisherIdentity", + "publisher_sid", + "publisherSid", + "track_sid", + "trackSid", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + PublisherIdentity, + PublisherSid, + TrackSid, + __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 { + "publisherIdentity" | "publisher_identity" => Ok(GeneratedField::PublisherIdentity), + "publisherSid" | "publisher_sid" => Ok(GeneratedField::PublisherSid), + "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = data_track_subscriber_handles::PublishedDataTrack; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackSubscriberHandles.PublishedDataTrack") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut publisher_identity__ = None; + let mut publisher_sid__ = None; + let mut track_sid__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::PublisherIdentity => { + if publisher_identity__.is_some() { + return Err(serde::de::Error::duplicate_field("publisherIdentity")); + } + publisher_identity__ = Some(map_.next_value()?); + } + GeneratedField::PublisherSid => { + if publisher_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("publisherSid")); + } + publisher_sid__ = Some(map_.next_value()?); + } + GeneratedField::TrackSid => { + if track_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSid")); + } + track_sid__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(data_track_subscriber_handles::PublishedDataTrack { + publisher_identity: publisher_identity__.unwrap_or_default(), + publisher_sid: publisher_sid__.unwrap_or_default(), + track_sid: track_sid__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackSubscriberHandles.PublishedDataTrack", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for DataTrackSubscriptionOptions { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -18556,15 +18874,23 @@ impl serde::Serialize for MetricsRecordingHeader { if !self.room_id.is_empty() { len += 1; } - if self.enable_user_data_training.is_some() { + if self.duration != 0 { + len += 1; + } + if self.start_time.is_some() { len += 1; } let mut struct_ser = serializer.serialize_struct("livekit.MetricsRecordingHeader", len)?; if !self.room_id.is_empty() { struct_ser.serialize_field("roomId", &self.room_id)?; } - if let Some(v) = self.enable_user_data_training.as_ref() { - struct_ser.serialize_field("enableUserDataTraining", v)?; + if self.duration != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("duration", ToString::to_string(&self.duration).as_str())?; + } + if let Some(v) = self.start_time.as_ref() { + struct_ser.serialize_field("startTime", v)?; } struct_ser.end() } @@ -18578,14 +18904,16 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { const FIELDS: &[&str] = &[ "room_id", "roomId", - "enable_user_data_training", - "enableUserDataTraining", + "duration", + "start_time", + "startTime", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { RoomId, - EnableUserDataTraining, + Duration, + StartTime, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -18609,7 +18937,8 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { { match value { "roomId" | "room_id" => Ok(GeneratedField::RoomId), - "enableUserDataTraining" | "enable_user_data_training" => Ok(GeneratedField::EnableUserDataTraining), + "duration" => Ok(GeneratedField::Duration), + "startTime" | "start_time" => Ok(GeneratedField::StartTime), _ => Ok(GeneratedField::__SkipField__), } } @@ -18630,7 +18959,8 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { V: serde::de::MapAccess<'de>, { let mut room_id__ = None; - let mut enable_user_data_training__ = None; + let mut duration__ = None; + let mut start_time__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::RoomId => { @@ -18639,11 +18969,19 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { } room_id__ = Some(map_.next_value()?); } - GeneratedField::EnableUserDataTraining => { - if enable_user_data_training__.is_some() { - return Err(serde::de::Error::duplicate_field("enableUserDataTraining")); + GeneratedField::Duration => { + if duration__.is_some() { + return Err(serde::de::Error::duplicate_field("duration")); } - enable_user_data_training__ = map_.next_value()?; + duration__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::StartTime => { + if start_time__.is_some() { + return Err(serde::de::Error::duplicate_field("startTime")); + } + start_time__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; @@ -18652,7 +18990,8 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { } Ok(MetricsRecordingHeader { room_id: room_id__.unwrap_or_default(), - enable_user_data_training: enable_user_data_training__, + duration: duration__.unwrap_or_default(), + start_time: start_time__, }) } } @@ -20169,6 +20508,8 @@ impl serde::Serialize for participant_info::KindDetail { let variant = match self { Self::CloudAgent => "CLOUD_AGENT", Self::Forwarded => "FORWARDED", + Self::ConnectorWhatsapp => "CONNECTOR_WHATSAPP", + Self::ConnectorTwilio => "CONNECTOR_TWILIO", }; serializer.serialize_str(variant) } @@ -20182,6 +20523,8 @@ impl<'de> serde::Deserialize<'de> for participant_info::KindDetail { const FIELDS: &[&str] = &[ "CLOUD_AGENT", "FORWARDED", + "CONNECTOR_WHATSAPP", + "CONNECTOR_TWILIO", ]; struct GeneratedVisitor; @@ -20224,6 +20567,8 @@ impl<'de> serde::Deserialize<'de> for participant_info::KindDetail { match value { "CLOUD_AGENT" => Ok(participant_info::KindDetail::CloudAgent), "FORWARDED" => Ok(participant_info::KindDetail::Forwarded), + "CONNECTOR_WHATSAPP" => Ok(participant_info::KindDetail::ConnectorWhatsapp), + "CONNECTOR_TWILIO" => Ok(participant_info::KindDetail::ConnectorTwilio), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -31130,11 +31475,15 @@ impl serde::Serialize for SipStatusCode { Self::SipStatusCallIsForwarded => "SIP_STATUS_CALL_IS_FORWARDED", Self::SipStatusQueued => "SIP_STATUS_QUEUED", Self::SipStatusSessionProgress => "SIP_STATUS_SESSION_PROGRESS", + Self::SipStatusEarlyDialogTerminated => "SIP_STATUS_EARLY_DIALOG_TERMINATED", Self::SipStatusOk => "SIP_STATUS_OK", Self::SipStatusAccepted => "SIP_STATUS_ACCEPTED", + Self::SipStatusNoNotification => "SIP_STATUS_NO_NOTIFICATION", + Self::SipStatusMultipleChoices => "SIP_STATUS_MULTIPLE_CHOICES", Self::SipStatusMovedPermanently => "SIP_STATUS_MOVED_PERMANENTLY", Self::SipStatusMovedTemporarily => "SIP_STATUS_MOVED_TEMPORARILY", Self::SipStatusUseProxy => "SIP_STATUS_USE_PROXY", + Self::SipStatusAlternativeService => "SIP_STATUS_ALTERNATIVE_SERVICE", Self::SipStatusBadRequest => "SIP_STATUS_BAD_REQUEST", Self::SipStatusUnauthorized => "SIP_STATUS_UNAUTHORIZED", Self::SipStatusPaymentRequired => "SIP_STATUS_PAYMENT_REQUIRED", @@ -31146,13 +31495,30 @@ impl serde::Serialize for SipStatusCode { Self::SipStatusRequestTimeout => "SIP_STATUS_REQUEST_TIMEOUT", Self::SipStatusConflict => "SIP_STATUS_CONFLICT", Self::SipStatusGone => "SIP_STATUS_GONE", + Self::SipStatusLengthRequired => "SIP_STATUS_LENGTH_REQUIRED", + Self::SipStatusConditionalRequestFailed => "SIP_STATUS_CONDITIONAL_REQUEST_FAILED", Self::SipStatusRequestEntityTooLarge => "SIP_STATUS_REQUEST_ENTITY_TOO_LARGE", Self::SipStatusRequestUriTooLong => "SIP_STATUS_REQUEST_URI_TOO_LONG", Self::SipStatusUnsupportedMediaType => "SIP_STATUS_UNSUPPORTED_MEDIA_TYPE", Self::SipStatusRequestedRangeNotSatisfiable => "SIP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE", + Self::SipStatusUnknownResourcePriority => "SIP_STATUS_UNKNOWN_RESOURCE_PRIORITY", Self::SipStatusBadExtension => "SIP_STATUS_BAD_EXTENSION", Self::SipStatusExtensionRequired => "SIP_STATUS_EXTENSION_REQUIRED", + Self::SipStatusSessionIntervalTooSmall => "SIP_STATUS_SESSION_INTERVAL_TOO_SMALL", Self::SipStatusIntervalTooBrief => "SIP_STATUS_INTERVAL_TOO_BRIEF", + Self::SipStatusBadLocationInformation => "SIP_STATUS_BAD_LOCATION_INFORMATION", + Self::SipStatusBadAlertMessage => "SIP_STATUS_BAD_ALERT_MESSAGE", + Self::SipStatusUseIdentityHeader => "SIP_STATUS_USE_IDENTITY_HEADER", + Self::SipStatusProvideReferrerIdentity => "SIP_STATUS_PROVIDE_REFERRER_IDENTITY", + Self::SipStatusFlowFailed => "SIP_STATUS_FLOW_FAILED", + Self::SipStatusAnonymityDisallowed => "SIP_STATUS_ANONYMITY_DISALLOWED", + Self::SipStatusBadIdentityInfo => "SIP_STATUS_BAD_IDENTITY_INFO", + Self::SipStatusUnsupportedCertificate => "SIP_STATUS_UNSUPPORTED_CERTIFICATE", + Self::SipStatusInvalidIdentityHeader => "SIP_STATUS_INVALID_IDENTITY_HEADER", + Self::SipStatusFirstHopLacksOutboundSupport => "SIP_STATUS_FIRST_HOP_LACKS_OUTBOUND_SUPPORT", + Self::SipStatusMaxBreadthExceeded => "SIP_STATUS_MAX_BREADTH_EXCEEDED", + Self::SipStatusBadInfoPackage => "SIP_STATUS_BAD_INFO_PACKAGE", + Self::SipStatusConsentNeeded => "SIP_STATUS_CONSENT_NEEDED", Self::SipStatusTemporarilyUnavailable => "SIP_STATUS_TEMPORARILY_UNAVAILABLE", Self::SipStatusCallTransactionDoesNotExists => "SIP_STATUS_CALL_TRANSACTION_DOES_NOT_EXISTS", Self::SipStatusLoopDetected => "SIP_STATUS_LOOP_DETECTED", @@ -31162,6 +31528,10 @@ impl serde::Serialize for SipStatusCode { Self::SipStatusBusyHere => "SIP_STATUS_BUSY_HERE", Self::SipStatusRequestTerminated => "SIP_STATUS_REQUEST_TERMINATED", Self::SipStatusNotAcceptableHere => "SIP_STATUS_NOT_ACCEPTABLE_HERE", + Self::SipStatusBadEvent => "SIP_STATUS_BAD_EVENT", + Self::SipStatusRequestPending => "SIP_STATUS_REQUEST_PENDING", + Self::SipStatusUndecipherable => "SIP_STATUS_UNDECIPHERABLE", + Self::SipStatusSecurityAgreementRequired => "SIP_STATUS_SECURITY_AGREEMENT_REQUIRED", Self::SipStatusInternalServerError => "SIP_STATUS_INTERNAL_SERVER_ERROR", Self::SipStatusNotImplemented => "SIP_STATUS_NOT_IMPLEMENTED", Self::SipStatusBadGateway => "SIP_STATUS_BAD_GATEWAY", @@ -31173,6 +31543,8 @@ impl serde::Serialize for SipStatusCode { Self::SipStatusGlobalDecline => "SIP_STATUS_GLOBAL_DECLINE", Self::SipStatusGlobalDoesNotExistAnywhere => "SIP_STATUS_GLOBAL_DOES_NOT_EXIST_ANYWHERE", Self::SipStatusGlobalNotAcceptable => "SIP_STATUS_GLOBAL_NOT_ACCEPTABLE", + Self::SipStatusGlobalUnwanted => "SIP_STATUS_GLOBAL_UNWANTED", + Self::SipStatusGlobalRejected => "SIP_STATUS_GLOBAL_REJECTED", }; serializer.serialize_str(variant) } @@ -31190,11 +31562,15 @@ impl<'de> serde::Deserialize<'de> for SipStatusCode { "SIP_STATUS_CALL_IS_FORWARDED", "SIP_STATUS_QUEUED", "SIP_STATUS_SESSION_PROGRESS", + "SIP_STATUS_EARLY_DIALOG_TERMINATED", "SIP_STATUS_OK", "SIP_STATUS_ACCEPTED", + "SIP_STATUS_NO_NOTIFICATION", + "SIP_STATUS_MULTIPLE_CHOICES", "SIP_STATUS_MOVED_PERMANENTLY", "SIP_STATUS_MOVED_TEMPORARILY", "SIP_STATUS_USE_PROXY", + "SIP_STATUS_ALTERNATIVE_SERVICE", "SIP_STATUS_BAD_REQUEST", "SIP_STATUS_UNAUTHORIZED", "SIP_STATUS_PAYMENT_REQUIRED", @@ -31206,13 +31582,30 @@ impl<'de> serde::Deserialize<'de> for SipStatusCode { "SIP_STATUS_REQUEST_TIMEOUT", "SIP_STATUS_CONFLICT", "SIP_STATUS_GONE", + "SIP_STATUS_LENGTH_REQUIRED", + "SIP_STATUS_CONDITIONAL_REQUEST_FAILED", "SIP_STATUS_REQUEST_ENTITY_TOO_LARGE", "SIP_STATUS_REQUEST_URI_TOO_LONG", "SIP_STATUS_UNSUPPORTED_MEDIA_TYPE", "SIP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE", + "SIP_STATUS_UNKNOWN_RESOURCE_PRIORITY", "SIP_STATUS_BAD_EXTENSION", "SIP_STATUS_EXTENSION_REQUIRED", + "SIP_STATUS_SESSION_INTERVAL_TOO_SMALL", "SIP_STATUS_INTERVAL_TOO_BRIEF", + "SIP_STATUS_BAD_LOCATION_INFORMATION", + "SIP_STATUS_BAD_ALERT_MESSAGE", + "SIP_STATUS_USE_IDENTITY_HEADER", + "SIP_STATUS_PROVIDE_REFERRER_IDENTITY", + "SIP_STATUS_FLOW_FAILED", + "SIP_STATUS_ANONYMITY_DISALLOWED", + "SIP_STATUS_BAD_IDENTITY_INFO", + "SIP_STATUS_UNSUPPORTED_CERTIFICATE", + "SIP_STATUS_INVALID_IDENTITY_HEADER", + "SIP_STATUS_FIRST_HOP_LACKS_OUTBOUND_SUPPORT", + "SIP_STATUS_MAX_BREADTH_EXCEEDED", + "SIP_STATUS_BAD_INFO_PACKAGE", + "SIP_STATUS_CONSENT_NEEDED", "SIP_STATUS_TEMPORARILY_UNAVAILABLE", "SIP_STATUS_CALL_TRANSACTION_DOES_NOT_EXISTS", "SIP_STATUS_LOOP_DETECTED", @@ -31222,6 +31615,10 @@ impl<'de> serde::Deserialize<'de> for SipStatusCode { "SIP_STATUS_BUSY_HERE", "SIP_STATUS_REQUEST_TERMINATED", "SIP_STATUS_NOT_ACCEPTABLE_HERE", + "SIP_STATUS_BAD_EVENT", + "SIP_STATUS_REQUEST_PENDING", + "SIP_STATUS_UNDECIPHERABLE", + "SIP_STATUS_SECURITY_AGREEMENT_REQUIRED", "SIP_STATUS_INTERNAL_SERVER_ERROR", "SIP_STATUS_NOT_IMPLEMENTED", "SIP_STATUS_BAD_GATEWAY", @@ -31233,6 +31630,8 @@ impl<'de> serde::Deserialize<'de> for SipStatusCode { "SIP_STATUS_GLOBAL_DECLINE", "SIP_STATUS_GLOBAL_DOES_NOT_EXIST_ANYWHERE", "SIP_STATUS_GLOBAL_NOT_ACCEPTABLE", + "SIP_STATUS_GLOBAL_UNWANTED", + "SIP_STATUS_GLOBAL_REJECTED", ]; struct GeneratedVisitor; @@ -31279,11 +31678,15 @@ impl<'de> serde::Deserialize<'de> for SipStatusCode { "SIP_STATUS_CALL_IS_FORWARDED" => Ok(SipStatusCode::SipStatusCallIsForwarded), "SIP_STATUS_QUEUED" => Ok(SipStatusCode::SipStatusQueued), "SIP_STATUS_SESSION_PROGRESS" => Ok(SipStatusCode::SipStatusSessionProgress), + "SIP_STATUS_EARLY_DIALOG_TERMINATED" => Ok(SipStatusCode::SipStatusEarlyDialogTerminated), "SIP_STATUS_OK" => Ok(SipStatusCode::SipStatusOk), "SIP_STATUS_ACCEPTED" => Ok(SipStatusCode::SipStatusAccepted), + "SIP_STATUS_NO_NOTIFICATION" => Ok(SipStatusCode::SipStatusNoNotification), + "SIP_STATUS_MULTIPLE_CHOICES" => Ok(SipStatusCode::SipStatusMultipleChoices), "SIP_STATUS_MOVED_PERMANENTLY" => Ok(SipStatusCode::SipStatusMovedPermanently), "SIP_STATUS_MOVED_TEMPORARILY" => Ok(SipStatusCode::SipStatusMovedTemporarily), "SIP_STATUS_USE_PROXY" => Ok(SipStatusCode::SipStatusUseProxy), + "SIP_STATUS_ALTERNATIVE_SERVICE" => Ok(SipStatusCode::SipStatusAlternativeService), "SIP_STATUS_BAD_REQUEST" => Ok(SipStatusCode::SipStatusBadRequest), "SIP_STATUS_UNAUTHORIZED" => Ok(SipStatusCode::SipStatusUnauthorized), "SIP_STATUS_PAYMENT_REQUIRED" => Ok(SipStatusCode::SipStatusPaymentRequired), @@ -31295,13 +31698,30 @@ impl<'de> serde::Deserialize<'de> for SipStatusCode { "SIP_STATUS_REQUEST_TIMEOUT" => Ok(SipStatusCode::SipStatusRequestTimeout), "SIP_STATUS_CONFLICT" => Ok(SipStatusCode::SipStatusConflict), "SIP_STATUS_GONE" => Ok(SipStatusCode::SipStatusGone), + "SIP_STATUS_LENGTH_REQUIRED" => Ok(SipStatusCode::SipStatusLengthRequired), + "SIP_STATUS_CONDITIONAL_REQUEST_FAILED" => Ok(SipStatusCode::SipStatusConditionalRequestFailed), "SIP_STATUS_REQUEST_ENTITY_TOO_LARGE" => Ok(SipStatusCode::SipStatusRequestEntityTooLarge), "SIP_STATUS_REQUEST_URI_TOO_LONG" => Ok(SipStatusCode::SipStatusRequestUriTooLong), "SIP_STATUS_UNSUPPORTED_MEDIA_TYPE" => Ok(SipStatusCode::SipStatusUnsupportedMediaType), "SIP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE" => Ok(SipStatusCode::SipStatusRequestedRangeNotSatisfiable), + "SIP_STATUS_UNKNOWN_RESOURCE_PRIORITY" => Ok(SipStatusCode::SipStatusUnknownResourcePriority), "SIP_STATUS_BAD_EXTENSION" => Ok(SipStatusCode::SipStatusBadExtension), "SIP_STATUS_EXTENSION_REQUIRED" => Ok(SipStatusCode::SipStatusExtensionRequired), + "SIP_STATUS_SESSION_INTERVAL_TOO_SMALL" => Ok(SipStatusCode::SipStatusSessionIntervalTooSmall), "SIP_STATUS_INTERVAL_TOO_BRIEF" => Ok(SipStatusCode::SipStatusIntervalTooBrief), + "SIP_STATUS_BAD_LOCATION_INFORMATION" => Ok(SipStatusCode::SipStatusBadLocationInformation), + "SIP_STATUS_BAD_ALERT_MESSAGE" => Ok(SipStatusCode::SipStatusBadAlertMessage), + "SIP_STATUS_USE_IDENTITY_HEADER" => Ok(SipStatusCode::SipStatusUseIdentityHeader), + "SIP_STATUS_PROVIDE_REFERRER_IDENTITY" => Ok(SipStatusCode::SipStatusProvideReferrerIdentity), + "SIP_STATUS_FLOW_FAILED" => Ok(SipStatusCode::SipStatusFlowFailed), + "SIP_STATUS_ANONYMITY_DISALLOWED" => Ok(SipStatusCode::SipStatusAnonymityDisallowed), + "SIP_STATUS_BAD_IDENTITY_INFO" => Ok(SipStatusCode::SipStatusBadIdentityInfo), + "SIP_STATUS_UNSUPPORTED_CERTIFICATE" => Ok(SipStatusCode::SipStatusUnsupportedCertificate), + "SIP_STATUS_INVALID_IDENTITY_HEADER" => Ok(SipStatusCode::SipStatusInvalidIdentityHeader), + "SIP_STATUS_FIRST_HOP_LACKS_OUTBOUND_SUPPORT" => Ok(SipStatusCode::SipStatusFirstHopLacksOutboundSupport), + "SIP_STATUS_MAX_BREADTH_EXCEEDED" => Ok(SipStatusCode::SipStatusMaxBreadthExceeded), + "SIP_STATUS_BAD_INFO_PACKAGE" => Ok(SipStatusCode::SipStatusBadInfoPackage), + "SIP_STATUS_CONSENT_NEEDED" => Ok(SipStatusCode::SipStatusConsentNeeded), "SIP_STATUS_TEMPORARILY_UNAVAILABLE" => Ok(SipStatusCode::SipStatusTemporarilyUnavailable), "SIP_STATUS_CALL_TRANSACTION_DOES_NOT_EXISTS" => Ok(SipStatusCode::SipStatusCallTransactionDoesNotExists), "SIP_STATUS_LOOP_DETECTED" => Ok(SipStatusCode::SipStatusLoopDetected), @@ -31311,6 +31731,10 @@ impl<'de> serde::Deserialize<'de> for SipStatusCode { "SIP_STATUS_BUSY_HERE" => Ok(SipStatusCode::SipStatusBusyHere), "SIP_STATUS_REQUEST_TERMINATED" => Ok(SipStatusCode::SipStatusRequestTerminated), "SIP_STATUS_NOT_ACCEPTABLE_HERE" => Ok(SipStatusCode::SipStatusNotAcceptableHere), + "SIP_STATUS_BAD_EVENT" => Ok(SipStatusCode::SipStatusBadEvent), + "SIP_STATUS_REQUEST_PENDING" => Ok(SipStatusCode::SipStatusRequestPending), + "SIP_STATUS_UNDECIPHERABLE" => Ok(SipStatusCode::SipStatusUndecipherable), + "SIP_STATUS_SECURITY_AGREEMENT_REQUIRED" => Ok(SipStatusCode::SipStatusSecurityAgreementRequired), "SIP_STATUS_INTERNAL_SERVER_ERROR" => Ok(SipStatusCode::SipStatusInternalServerError), "SIP_STATUS_NOT_IMPLEMENTED" => Ok(SipStatusCode::SipStatusNotImplemented), "SIP_STATUS_BAD_GATEWAY" => Ok(SipStatusCode::SipStatusBadGateway), @@ -31322,6 +31746,8 @@ impl<'de> serde::Deserialize<'de> for SipStatusCode { "SIP_STATUS_GLOBAL_DECLINE" => Ok(SipStatusCode::SipStatusGlobalDecline), "SIP_STATUS_GLOBAL_DOES_NOT_EXIST_ANYWHERE" => Ok(SipStatusCode::SipStatusGlobalDoesNotExistAnywhere), "SIP_STATUS_GLOBAL_NOT_ACCEPTABLE" => Ok(SipStatusCode::SipStatusGlobalNotAcceptable), + "SIP_STATUS_GLOBAL_UNWANTED" => Ok(SipStatusCode::SipStatusGlobalUnwanted), + "SIP_STATUS_GLOBAL_REJECTED" => Ok(SipStatusCode::SipStatusGlobalRejected), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -33880,11 +34306,11 @@ impl serde::Serialize for SignalRequest { signal_request::Message::UpdateVideoTrack(v) => { struct_ser.serialize_field("updateVideoTrack", v)?; } - signal_request::Message::PublishDataTrack(v) => { - struct_ser.serialize_field("publishDataTrack", v)?; + signal_request::Message::PublishDataTrackRequest(v) => { + struct_ser.serialize_field("publishDataTrackRequest", v)?; } - signal_request::Message::UnpublishDataTrack(v) => { - struct_ser.serialize_field("unpublishDataTrack", v)?; + signal_request::Message::UnpublishDataTrackRequest(v) => { + struct_ser.serialize_field("unpublishDataTrackRequest", v)?; } signal_request::Message::UpdateDataSubscription(v) => { struct_ser.serialize_field("updateDataSubscription", v)?; @@ -33927,10 +34353,10 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "updateAudioTrack", "update_video_track", "updateVideoTrack", - "publish_data_track", - "publishDataTrack", - "unpublish_data_track", - "unpublishDataTrack", + "publish_data_track_request", + "publishDataTrackRequest", + "unpublish_data_track_request", + "unpublishDataTrackRequest", "update_data_subscription", "updateDataSubscription", ]; @@ -33954,8 +34380,8 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { PingReq, UpdateAudioTrack, UpdateVideoTrack, - PublishDataTrack, - UnpublishDataTrack, + PublishDataTrackRequest, + UnpublishDataTrackRequest, UpdateDataSubscription, __SkipField__, } @@ -33996,8 +34422,8 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "pingReq" | "ping_req" => Ok(GeneratedField::PingReq), "updateAudioTrack" | "update_audio_track" => Ok(GeneratedField::UpdateAudioTrack), "updateVideoTrack" | "update_video_track" => Ok(GeneratedField::UpdateVideoTrack), - "publishDataTrack" | "publish_data_track" => Ok(GeneratedField::PublishDataTrack), - "unpublishDataTrack" | "unpublish_data_track" => Ok(GeneratedField::UnpublishDataTrack), + "publishDataTrackRequest" | "publish_data_track_request" => Ok(GeneratedField::PublishDataTrackRequest), + "unpublishDataTrackRequest" | "unpublish_data_track_request" => Ok(GeneratedField::UnpublishDataTrackRequest), "updateDataSubscription" | "update_data_subscription" => Ok(GeneratedField::UpdateDataSubscription), _ => Ok(GeneratedField::__SkipField__), } @@ -34139,18 +34565,18 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UpdateVideoTrack) ; } - GeneratedField::PublishDataTrack => { + GeneratedField::PublishDataTrackRequest => { if message__.is_some() { - return Err(serde::de::Error::duplicate_field("publishDataTrack")); + return Err(serde::de::Error::duplicate_field("publishDataTrackRequest")); } - message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::PublishDataTrack) + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::PublishDataTrackRequest) ; } - GeneratedField::UnpublishDataTrack => { + GeneratedField::UnpublishDataTrackRequest => { if message__.is_some() { - return Err(serde::de::Error::duplicate_field("unpublishDataTrack")); + return Err(serde::de::Error::duplicate_field("unpublishDataTrackRequest")); } - message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UnpublishDataTrack) + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UnpublishDataTrackRequest) ; } GeneratedField::UpdateDataSubscription => { @@ -34264,14 +34690,14 @@ impl serde::Serialize for SignalResponse { signal_response::Message::SubscribedAudioCodecUpdate(v) => { struct_ser.serialize_field("subscribedAudioCodecUpdate", v)?; } - signal_response::Message::PublishDataTrack(v) => { - struct_ser.serialize_field("publishDataTrack", v)?; + signal_response::Message::PublishDataTrackResponse(v) => { + struct_ser.serialize_field("publishDataTrackResponse", v)?; } - signal_response::Message::UnpublishDataTrack(v) => { - struct_ser.serialize_field("unpublishDataTrack", v)?; + signal_response::Message::UnpublishDataTrackResponse(v) => { + struct_ser.serialize_field("unpublishDataTrackResponse", v)?; } - signal_response::Message::DataTrackHandles(v) => { - struct_ser.serialize_field("dataTrackHandles", v)?; + signal_response::Message::DataTrackSubscriberHandles(v) => { + struct_ser.serialize_field("dataTrackSubscriberHandles", v)?; } } } @@ -34326,12 +34752,12 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "mediaSectionsRequirement", "subscribed_audio_codec_update", "subscribedAudioCodecUpdate", - "publish_data_track", - "publishDataTrack", - "unpublish_data_track", - "unpublishDataTrack", - "data_track_handles", - "dataTrackHandles", + "publish_data_track_response", + "publishDataTrackResponse", + "unpublish_data_track_response", + "unpublishDataTrackResponse", + "data_track_subscriber_handles", + "dataTrackSubscriberHandles", ]; #[allow(clippy::enum_variant_names)] @@ -34361,9 +34787,9 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { RoomMoved, MediaSectionsRequirement, SubscribedAudioCodecUpdate, - PublishDataTrack, - UnpublishDataTrack, - DataTrackHandles, + PublishDataTrackResponse, + UnpublishDataTrackResponse, + DataTrackSubscriberHandles, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -34411,9 +34837,9 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "roomMoved" | "room_moved" => Ok(GeneratedField::RoomMoved), "mediaSectionsRequirement" | "media_sections_requirement" => Ok(GeneratedField::MediaSectionsRequirement), "subscribedAudioCodecUpdate" | "subscribed_audio_codec_update" => Ok(GeneratedField::SubscribedAudioCodecUpdate), - "publishDataTrack" | "publish_data_track" => Ok(GeneratedField::PublishDataTrack), - "unpublishDataTrack" | "unpublish_data_track" => Ok(GeneratedField::UnpublishDataTrack), - "dataTrackHandles" | "data_track_handles" => Ok(GeneratedField::DataTrackHandles), + "publishDataTrackResponse" | "publish_data_track_response" => Ok(GeneratedField::PublishDataTrackResponse), + "unpublishDataTrackResponse" | "unpublish_data_track_response" => Ok(GeneratedField::UnpublishDataTrackResponse), + "dataTrackSubscriberHandles" | "data_track_subscriber_handles" => Ok(GeneratedField::DataTrackSubscriberHandles), _ => Ok(GeneratedField::__SkipField__), } } @@ -34609,25 +35035,25 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::SubscribedAudioCodecUpdate) ; } - GeneratedField::PublishDataTrack => { + GeneratedField::PublishDataTrackResponse => { if message__.is_some() { - return Err(serde::de::Error::duplicate_field("publishDataTrack")); + return Err(serde::de::Error::duplicate_field("publishDataTrackResponse")); } - message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::PublishDataTrack) + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::PublishDataTrackResponse) ; } - GeneratedField::UnpublishDataTrack => { + GeneratedField::UnpublishDataTrackResponse => { if message__.is_some() { - return Err(serde::de::Error::duplicate_field("unpublishDataTrack")); + return Err(serde::de::Error::duplicate_field("unpublishDataTrackResponse")); } - message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::UnpublishDataTrack) + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::UnpublishDataTrackResponse) ; } - GeneratedField::DataTrackHandles => { + GeneratedField::DataTrackSubscriberHandles => { if message__.is_some() { - return Err(serde::de::Error::duplicate_field("dataTrackHandles")); + return Err(serde::de::Error::duplicate_field("dataTrackSubscriberHandles")); } - message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::DataTrackHandles) + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::DataTrackSubscriberHandles) ; } GeneratedField::__SkipField__ => { @@ -35848,6 +36274,12 @@ impl serde::Serialize for StreamInfo { if !self.error.is_empty() { len += 1; } + if self.last_retry_at != 0 { + len += 1; + } + if self.retries != 0 { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.StreamInfo", len)?; if !self.url.is_empty() { struct_ser.serialize_field("url", &self.url)?; @@ -35875,6 +36307,14 @@ impl serde::Serialize for StreamInfo { if !self.error.is_empty() { struct_ser.serialize_field("error", &self.error)?; } + if self.last_retry_at != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("lastRetryAt", ToString::to_string(&self.last_retry_at).as_str())?; + } + if self.retries != 0 { + struct_ser.serialize_field("retries", &self.retries)?; + } struct_ser.end() } } @@ -35893,6 +36333,9 @@ impl<'de> serde::Deserialize<'de> for StreamInfo { "duration", "status", "error", + "last_retry_at", + "lastRetryAt", + "retries", ]; #[allow(clippy::enum_variant_names)] @@ -35903,6 +36346,8 @@ impl<'de> serde::Deserialize<'de> for StreamInfo { Duration, Status, Error, + LastRetryAt, + Retries, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -35931,6 +36376,8 @@ impl<'de> serde::Deserialize<'de> for StreamInfo { "duration" => Ok(GeneratedField::Duration), "status" => Ok(GeneratedField::Status), "error" => Ok(GeneratedField::Error), + "lastRetryAt" | "last_retry_at" => Ok(GeneratedField::LastRetryAt), + "retries" => Ok(GeneratedField::Retries), _ => Ok(GeneratedField::__SkipField__), } } @@ -35956,6 +36403,8 @@ impl<'de> serde::Deserialize<'de> for StreamInfo { let mut duration__ = None; let mut status__ = None; let mut error__ = None; + let mut last_retry_at__ = None; + let mut retries__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Url => { @@ -36000,6 +36449,22 @@ impl<'de> serde::Deserialize<'de> for StreamInfo { } error__ = Some(map_.next_value()?); } + GeneratedField::LastRetryAt => { + if last_retry_at__.is_some() { + return Err(serde::de::Error::duplicate_field("lastRetryAt")); + } + last_retry_at__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Retries => { + if retries__.is_some() { + return Err(serde::de::Error::duplicate_field("retries")); + } + retries__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -36012,6 +36477,8 @@ impl<'de> serde::Deserialize<'de> for StreamInfo { duration: duration__.unwrap_or_default(), status: status__.unwrap_or_default(), error: error__.unwrap_or_default(), + last_retry_at: last_retry_at__.unwrap_or_default(), + retries: retries__.unwrap_or_default(), }) } } @@ -37722,6 +38189,9 @@ impl serde::Serialize for SyncState { if !self.datachannel_receive_states.is_empty() { len += 1; } + if !self.publish_data_tracks.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.SyncState", len)?; if let Some(v) = self.answer.as_ref() { struct_ser.serialize_field("answer", v)?; @@ -37744,6 +38214,9 @@ impl serde::Serialize for SyncState { if !self.datachannel_receive_states.is_empty() { struct_ser.serialize_field("datachannelReceiveStates", &self.datachannel_receive_states)?; } + if !self.publish_data_tracks.is_empty() { + struct_ser.serialize_field("publishDataTracks", &self.publish_data_tracks)?; + } struct_ser.end() } } @@ -37765,6 +38238,8 @@ impl<'de> serde::Deserialize<'de> for SyncState { "trackSidsDisabled", "datachannel_receive_states", "datachannelReceiveStates", + "publish_data_tracks", + "publishDataTracks", ]; #[allow(clippy::enum_variant_names)] @@ -37776,6 +38251,7 @@ impl<'de> serde::Deserialize<'de> for SyncState { Offer, TrackSidsDisabled, DatachannelReceiveStates, + PublishDataTracks, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -37805,6 +38281,7 @@ impl<'de> serde::Deserialize<'de> for SyncState { "offer" => Ok(GeneratedField::Offer), "trackSidsDisabled" | "track_sids_disabled" => Ok(GeneratedField::TrackSidsDisabled), "datachannelReceiveStates" | "datachannel_receive_states" => Ok(GeneratedField::DatachannelReceiveStates), + "publishDataTracks" | "publish_data_tracks" => Ok(GeneratedField::PublishDataTracks), _ => Ok(GeneratedField::__SkipField__), } } @@ -37831,6 +38308,7 @@ impl<'de> serde::Deserialize<'de> for SyncState { let mut offer__ = None; let mut track_sids_disabled__ = None; let mut datachannel_receive_states__ = None; + let mut publish_data_tracks__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Answer => { @@ -37875,6 +38353,12 @@ impl<'de> serde::Deserialize<'de> for SyncState { } datachannel_receive_states__ = Some(map_.next_value()?); } + GeneratedField::PublishDataTracks => { + if publish_data_tracks__.is_some() { + return Err(serde::de::Error::duplicate_field("publishDataTracks")); + } + publish_data_tracks__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -37888,6 +38372,7 @@ impl<'de> serde::Deserialize<'de> for SyncState { offer: offer__, track_sids_disabled: track_sids_disabled__.unwrap_or_default(), datachannel_receive_states: datachannel_receive_states__.unwrap_or_default(), + publish_data_tracks: publish_data_tracks__.unwrap_or_default(), }) } } @@ -40544,12 +41029,12 @@ impl serde::Serialize for UnpublishDataTrackResponse { { use serde::ser::SerializeStruct; let mut len = 0; - if self.pub_handle != 0 { + if self.info.is_some() { len += 1; } let mut struct_ser = serializer.serialize_struct("livekit.UnpublishDataTrackResponse", len)?; - if self.pub_handle != 0 { - struct_ser.serialize_field("pubHandle", &self.pub_handle)?; + if let Some(v) = self.info.as_ref() { + struct_ser.serialize_field("info", v)?; } struct_ser.end() } @@ -40561,13 +41046,12 @@ impl<'de> serde::Deserialize<'de> for UnpublishDataTrackResponse { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "pub_handle", - "pubHandle", + "info", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - PubHandle, + Info, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -40590,7 +41074,7 @@ impl<'de> serde::Deserialize<'de> for UnpublishDataTrackResponse { E: serde::de::Error, { match value { - "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), + "info" => Ok(GeneratedField::Info), _ => Ok(GeneratedField::__SkipField__), } } @@ -40610,16 +41094,14 @@ impl<'de> serde::Deserialize<'de> for UnpublishDataTrackResponse { where V: serde::de::MapAccess<'de>, { - let mut pub_handle__ = None; + let mut info__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::PubHandle => { - if pub_handle__.is_some() { - return Err(serde::de::Error::duplicate_field("pubHandle")); + GeneratedField::Info => { + if info__.is_some() { + return Err(serde::de::Error::duplicate_field("info")); } - pub_handle__ = - Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) - ; + info__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; @@ -40627,7 +41109,7 @@ impl<'de> serde::Deserialize<'de> for UnpublishDataTrackResponse { } } Ok(UnpublishDataTrackResponse { - pub_handle: pub_handle__.unwrap_or_default(), + info: info__, }) } } @@ -40737,10 +41219,7 @@ impl serde::Serialize for update_data_subscription::Update { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.sid.is_empty() { - len += 1; - } - if !self.participant_identity.is_empty() { + if !self.track_sid.is_empty() { len += 1; } if self.subscribe { @@ -40750,11 +41229,8 @@ impl serde::Serialize for update_data_subscription::Update { len += 1; } let mut struct_ser = serializer.serialize_struct("livekit.UpdateDataSubscription.Update", len)?; - if !self.sid.is_empty() { - struct_ser.serialize_field("sid", &self.sid)?; - } - if !self.participant_identity.is_empty() { - struct_ser.serialize_field("participantIdentity", &self.participant_identity)?; + if !self.track_sid.is_empty() { + struct_ser.serialize_field("trackSid", &self.track_sid)?; } if self.subscribe { struct_ser.serialize_field("subscribe", &self.subscribe)?; @@ -40772,17 +41248,15 @@ impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "sid", - "participant_identity", - "participantIdentity", + "track_sid", + "trackSid", "subscribe", "options", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Sid, - ParticipantIdentity, + TrackSid, Subscribe, Options, __SkipField__, @@ -40807,8 +41281,7 @@ impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { E: serde::de::Error, { match value { - "sid" => Ok(GeneratedField::Sid), - "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), + "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), "subscribe" => Ok(GeneratedField::Subscribe), "options" => Ok(GeneratedField::Options), _ => Ok(GeneratedField::__SkipField__), @@ -40830,23 +41303,16 @@ impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { where V: serde::de::MapAccess<'de>, { - let mut sid__ = None; - let mut participant_identity__ = None; + let mut track_sid__ = None; let mut subscribe__ = None; let mut options__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Sid => { - if sid__.is_some() { - return Err(serde::de::Error::duplicate_field("sid")); - } - sid__ = Some(map_.next_value()?); - } - GeneratedField::ParticipantIdentity => { - if participant_identity__.is_some() { - return Err(serde::de::Error::duplicate_field("participantIdentity")); + GeneratedField::TrackSid => { + if track_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSid")); } - participant_identity__ = Some(map_.next_value()?); + track_sid__ = Some(map_.next_value()?); } GeneratedField::Subscribe => { if subscribe__.is_some() { @@ -40866,8 +41332,7 @@ impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { } } Ok(update_data_subscription::Update { - sid: sid__.unwrap_or_default(), - participant_identity: participant_identity__.unwrap_or_default(), + track_sid: track_sid__.unwrap_or_default(), subscribe: subscribe__.unwrap_or_default(), options: options__, }) From 377b314ae2007464890a0cdcb176dc9d7a4d0557 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:29:05 +1100 Subject: [PATCH 027/232] Apply lint suggestions --- livekit-datatrack/src/dtp/packet.rs | 2 +- livekit-datatrack/src/dtp/serialize.rs | 2 +- livekit-datatrack/src/dtp/track_handle.rs | 14 +++++++------- livekit-datatrack/src/manager/publish.rs | 21 ++++++++++++--------- livekit-datatrack/src/track.rs | 2 +- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/livekit-datatrack/src/dtp/packet.rs b/livekit-datatrack/src/dtp/packet.rs index 073605bf2..112fafd94 100644 --- a/livekit-datatrack/src/dtp/packet.rs +++ b/livekit-datatrack/src/dtp/packet.rs @@ -94,6 +94,6 @@ pub(crate) mod consts { pub const EXT_MARKER_USER_TIMESTAMP: u8 = ext_marker(EXT_ID_USER_TIMESTAMP, EXT_LEN_USER_TIMESTAMP as u8); const fn ext_marker(id: u8, len: u8) -> u8 { - (id << 4) | len - 1 + (id << 4) | (len - 1) } } diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/dtp/serialize.rs index 5f2fe3579..baa411792 100644 --- a/livekit-datatrack/src/dtp/serialize.rs +++ b/livekit-datatrack/src/dtp/serialize.rs @@ -126,7 +126,7 @@ impl Header { let ext_len = self.ext_len(); let ext_words = ext_len.div_ceil(4); assert!(ext_words <= u8::MAX.into()); - let padding_len = (ext_words as usize * 4) - ext_len; + let padding_len = (ext_words * 4) - ext_len; let len = BASE_HEADER_LEN + ext_len + padding_len; HeaderMetrics { ext_words, diff --git a/livekit-datatrack/src/dtp/track_handle.rs b/livekit-datatrack/src/dtp/track_handle.rs index 9b311a420..e162e5b78 100644 --- a/livekit-datatrack/src/dtp/track_handle.rs +++ b/livekit-datatrack/src/dtp/track_handle.rs @@ -44,19 +44,19 @@ impl TryFrom for TrackHandle { fn try_from(value: u32) -> Result { let value: u16 = value.try_into().map_err(|_| TrackHandleError::TooLarge)?; - Ok(value.try_into()?) + value.try_into() } } -impl Into for TrackHandle { - fn into(self) -> u16 { - self.0 +impl From for u16 { + fn from(handle: TrackHandle) -> Self { + handle.0 } } -impl Into for TrackHandle { - fn into(self) -> u32 { - self.0 as u32 +impl From for u32 { + fn from(handle: TrackHandle) -> Self { + handle.0 as u32 } } diff --git a/livekit-datatrack/src/manager/publish.rs b/livekit-datatrack/src/manager/publish.rs index 13dc64456..a85653f35 100644 --- a/livekit-datatrack/src/manager/publish.rs +++ b/livekit-datatrack/src/manager/publish.rs @@ -88,7 +88,10 @@ impl TrackPubTask { let mut state = DataTrackState::Published; while matches!(state, DataTrackState::Published) { tokio::select! { - _ = self.state_rx.changed() => Ok(state = *self.state_rx.borrow()), + _ = self.state_rx.changed() => { + let _: () = state = *self.state_rx.borrow(); + Ok(()) + }, Some(frame) = self.frame_rx.recv() => self.publish_frame(frame), else => break } @@ -238,7 +241,7 @@ impl PubManagerTask { return Ok(()); }; - if !self.pending_publications.insert(handle, req.result_tx).is_none() { + if self.pending_publications.insert(handle, req.result_tx).is_some() { Err(anyhow!("Publication already pending for handle"))? } @@ -265,7 +268,7 @@ impl PubManagerTask { Err(anyhow!("No pending track publication for {}", info.handle))? }; let track = self.create_local_track(info); - res_tx.send(Ok(track)); + let _ = res_tx.send(Ok(track)); Ok(()) } @@ -390,12 +393,12 @@ impl DataTrackOptions { } } -impl Into for proto::request_response::Reason { - fn into(self) -> PublishError { +impl From for PublishError { + fn from(reason: proto::request_response::Reason) -> Self { use proto::request_response::Reason; // If new error cases are added in the future, consider if they should // be treated as internal errors or added to the public error enum. - match self { + match reason { Reason::NotAllowed => PublishError::NotAllowed, Reason::DuplicateName => PublishError::DuplicateName, other => PublishError::Internal(anyhow!("SFU rejected: {:?}", other).into()), @@ -425,10 +428,10 @@ impl TryInto for proto::DataTrackInfo { } } -impl Into for PubSignalOutput { - fn into(self) -> proto::signal_request::Message { +impl From for proto::signal_request::Message { + fn from(output: PubSignalOutput) -> Self { use proto::signal_request::Message; - match self { + match output { PubSignalOutput::PublishRequest(req) => Message::PublishDataTrackRequest(req), PubSignalOutput::UnpublishRequest(req) => Message::UnpublishDataTrackRequest(req), } diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index 83adeb246..67069b079 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -107,7 +107,7 @@ impl DataTrack { /// Publish a frame onto the track. pub fn publish(&self, frame: impl Into) -> Result<(), PublishFrameError> { - Ok(self.handle().publish(frame.into())?) + self.handle().publish(frame.into()) } /// Whether or not the track is still published. From bea719f82d5753ac96e8b523c07420d8d559287d Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:48:22 +1100 Subject: [PATCH 028/232] TODO for sync state --- livekit-datatrack/src/dtp/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index 65aeff7b2..7619ccb62 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -17,6 +17,7 @@ mod packetizer; mod track_handle; mod deserialize; mod serialize; +mod utils; pub use packet::*; pub use packetizer::*; From 80a614b9c801750bac4796e55f7765b5daea2a86 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 20 Dec 2025 15:21:28 +1100 Subject: [PATCH 029/232] Implement packetizer --- Cargo.lock | 52 ++++++++++ livekit-datatrack/Cargo.toml | 4 + livekit-datatrack/src/dtp/deserialize.rs | 10 +- livekit-datatrack/src/dtp/mod.rs | 2 +- livekit-datatrack/src/dtp/packet.rs | 9 +- livekit-datatrack/src/dtp/packetizer.rs | 107 +++++++++++++++++++-- livekit-datatrack/src/dtp/serialize.rs | 24 ++--- livekit-datatrack/src/dtp/time.rs | 116 +++++++++++++++++++++++ livekit-datatrack/src/lib.rs | 3 +- livekit-datatrack/src/manager/publish.rs | 3 +- livekit-datatrack/src/utils/bytes.rs | 89 +++++++++++++++++ livekit-datatrack/src/utils/counter.rs | 39 ++++++++ livekit-datatrack/src/utils/mod.rs | 21 ++++ livekit/src/room/mod.rs | 2 + 14 files changed, 447 insertions(+), 34 deletions(-) create mode 100644 livekit-datatrack/src/dtp/time.rs create mode 100644 livekit-datatrack/src/utils/bytes.rs create mode 100644 livekit-datatrack/src/utils/counter.rs create mode 100644 livekit-datatrack/src/utils/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 80d50abb4..850889170 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1968,6 +1968,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -3075,6 +3081,8 @@ dependencies = [ "livekit-protocol", "livekit-runtime", "log", + "rand 0.9.2", + "rstest", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -4598,6 +4606,12 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -4687,6 +4701,35 @@ dependencies = [ "tokio", ] +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.110", + "unicode-ident", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -4705,6 +4748,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" diff --git a/livekit-datatrack/Cargo.toml b/livekit-datatrack/Cargo.toml index d366b2538..00acaff79 100644 --- a/livekit-datatrack/Cargo.toml +++ b/livekit-datatrack/Cargo.toml @@ -17,3 +17,7 @@ bytes = "1.10.1" from_variants = "1.0.2" tokio-stream = "0.1.17" anyhow = "1.0.100" # For internal error handling only +rand = "0.9.2" + +[dev-dependencies] +rstest = "0.26.1" diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs index 1932d6b65..d850bdfc4 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::dtp::time::Timestamp; + use super::{ - packet::{Dtp, E2ee, Header, consts::*}, + packet::{consts::*, Dtp, E2ee, Header}, track_handle::{TrackHandle, TrackHandleError}, }; use bytes::{Buf, Bytes}; @@ -74,7 +76,7 @@ impl Header { let track_handle: TrackHandle = raw.get_u16().try_into()?; let sequence = raw.get_u16(); let frame_number = raw.get_u16(); - let timestamp = raw.get_u32(); + let timestamp = Timestamp::from_ticks(raw.get_u32()); if ext_len > raw.remaining() { Err(DeserializeError::HeaderOverrun)? @@ -193,7 +195,7 @@ mod tests { assert_eq!(dtp.header.track_handle, 0x8811u32.try_into().unwrap()); assert_eq!(dtp.header.sequence, 0x4422); assert_eq!(dtp.header.frame_number, 0x4411); - assert_eq!(dtp.header.timestamp, 0x44221188); + assert_eq!(dtp.header.timestamp, Timestamp::from_ticks(0x44221188)); assert_eq!(dtp.header.user_timestamp, None); assert_eq!(dtp.header.e2ee, None); } @@ -228,7 +230,7 @@ mod tests { raw.put_u8(0x27); // ID 2, length 7 raw.put_slice(&[0x44, 0x11, 0x22, 0x11, 0x11, 0x11, 0x88, 0x11]); // User timestamp raw.put_bytes(0x00, 3); // Padding - // TODO: decreasing to 2 is header overrun (should be padding error) + // TODO: decreasing to 2 is header overrun (should be padding error) let dtp = Dtp::deserialize(raw.freeze()).unwrap(); assert_eq!(dtp.header.user_timestamp, Some(0x4411221111118811)); } diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index 7619ccb62..cc0b911f3 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -17,7 +17,7 @@ mod packetizer; mod track_handle; mod deserialize; mod serialize; -mod utils; +mod time; pub use packet::*; pub use packetizer::*; diff --git a/livekit-datatrack/src/dtp/packet.rs b/livekit-datatrack/src/dtp/packet.rs index 112fafd94..503f993a1 100644 --- a/livekit-datatrack/src/dtp/packet.rs +++ b/livekit-datatrack/src/dtp/packet.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::dtp::time::Timestamp; + use super::track_handle::TrackHandle; use bytes::Bytes; use core::fmt; @@ -29,12 +31,12 @@ pub struct Header { pub track_handle: TrackHandle, pub sequence: u16, pub frame_number: u16, - pub timestamp: u32, + pub timestamp: Timestamp<90_000>, pub user_timestamp: Option, pub e2ee: Option, } -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct E2ee { pub key_index: u8, pub iv: [u8; 12], @@ -91,7 +93,8 @@ pub(crate) mod consts { // Extension markers pub const EXT_MARKER_LEN: usize = 1; pub const EXT_MARKER_E2EE: u8 = ext_marker(EXT_ID_E2EE, EXT_LEN_E2EE as u8); - pub const EXT_MARKER_USER_TIMESTAMP: u8 = ext_marker(EXT_ID_USER_TIMESTAMP, EXT_LEN_USER_TIMESTAMP as u8); + pub const EXT_MARKER_USER_TIMESTAMP: u8 = + ext_marker(EXT_ID_USER_TIMESTAMP, EXT_LEN_USER_TIMESTAMP as u8); const fn ext_marker(id: u8, len: u8) -> u8 { (id << 4) | (len - 1) diff --git a/livekit-datatrack/src/dtp/packetizer.rs b/livekit-datatrack/src/dtp/packetizer.rs index dbb13ea66..11f56fe2a 100644 --- a/livekit-datatrack/src/dtp/packetizer.rs +++ b/livekit-datatrack/src/dtp/packetizer.rs @@ -12,33 +12,122 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::dtp::{Dtp, E2ee, TrackHandle}; +use crate::{ + dtp::{ + time::{Clock, Timestamp}, + Dtp, E2ee, Header, TrackHandle, + }, + utils::{BytesChunkExt, Counter}, +}; use bytes::Bytes; +use thiserror::Error; /// Converts application-level frames into packets for transport. +#[derive(Debug)] pub struct Packetizer { track_handle: TrackHandle, mtu_size: usize, - sequence: u16, - frame_number: u16, - timestamp: u32, + sequence: Counter, + frame_number: Counter, + clock: Clock<90_000>, } -/// Application-level frame packetized by [`Packetizer`]. +/// Frame packetized by [`Packetizer`]. pub struct PacketizerFrame { pub payload: Bytes, pub e2ee: Option, pub user_timestamp: Option, } +#[derive(Error, Debug)] +pub enum PacketizerError { + #[error("MTU is too short to send frame")] + MtuTooShort, +} + impl Packetizer { /// Creates a new packetizer. pub fn new(track_handle: TrackHandle, mtu_size: usize) -> Self { - Self { mtu_size, track_handle, sequence: 0, frame_number: 0, timestamp: 0 } + Self { + track_handle, + mtu_size, + sequence: Default::default(), + frame_number: Default::default(), + clock: Clock::new(Timestamp::random()), + } } /// Packetizes a frame into one or more packets. - pub fn packetize(&mut self, frame: PacketizerFrame) -> impl IntoIterator { - vec![] // TODO: + pub fn packetize(&mut self, frame: PacketizerFrame) -> Result, PacketizerError> { + // TODO: consider using default + let header = Header { + version: 0, + is_final: false, + track_handle: self.track_handle, + sequence: 0, + frame_number: self.frame_number.get_then_increment(), + timestamp: self.clock.now(), + user_timestamp: frame.user_timestamp, + e2ee: frame.e2ee, + }; + let max_payload_size = self.mtu_size.saturating_sub(header.serialized_len()); + if max_payload_size == 0 { + Err(PacketizerError::MtuTooShort)? + } + + let packet_payloads: Vec<_> = frame.payload.into_chunks(max_payload_size).collect(); + let packet_count = packet_payloads.len(); + let packets = packet_payloads + .into_iter() + .enumerate() + .map(|(index, payload)| Dtp { + header: Header { + is_final: index == packet_count - 1, + sequence: self.sequence.get_then_increment(), + ..header + }, + payload, + }) + .collect(); + Ok(packets) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest] + fn test_packetize( + #[values(0, 32, 784)] payload_size: usize, + #[values(256, 1024)] mtu_size: usize, + #[values(true, false)] with_exts: bool, + ) { + let handle = 1u32.try_into().unwrap(); + let e2ee = E2ee { key_index: 255, iv: [0xCD; 12] }; + let user_timestamp = u64::MAX; + + let mut packetizer = Packetizer::new(handle, mtu_size); + + let frame = PacketizerFrame { + payload: Bytes::from(vec![0xAB; payload_size]), + e2ee: with_exts.then_some(e2ee), + user_timestamp: with_exts.then_some(user_timestamp), + }; + let packets = packetizer.packetize(frame).expect("Failed to packetize"); + + if packets.len() == 0 { + assert_eq!(payload_size, 0, "Should be no packets for zero payload"); + return; + } + + for (index, packet) in packets.iter().enumerate() { + assert_eq!(packet.header.frame_number, 0); + assert_eq!(packet.header.sequence, index as u16); + assert_eq!(packet.header.e2ee, with_exts.then_some(e2ee)); + assert_eq!(packet.header.user_timestamp, with_exts.then_some(user_timestamp)); + } + assert!(packets.last().unwrap().is_final()); } -} \ No newline at end of file +} diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/dtp/serialize.rs index baa411792..6b4d8be2a 100644 --- a/livekit-datatrack/src/dtp/serialize.rs +++ b/livekit-datatrack/src/dtp/serialize.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::packet::{Dtp, Header, consts::*}; +use super::packet::{consts::*, Dtp, Header}; use bytes::{BufMut, Bytes, BytesMut}; use thiserror::Error; @@ -76,7 +76,7 @@ impl Header { buf.put_u16(self.track_handle.into()); buf.put_u16(self.sequence); buf.put_u16(self.frame_number); - buf.put_u32(self.timestamp); + buf.put_u32(self.timestamp.as_ticks()); if let Some(e2ee) = &self.e2ee { buf.put_u8(EXT_MARKER_E2EE); @@ -105,7 +105,7 @@ struct HeaderMetrics { impl Header { /// Length of the serialized header in bytes. - fn serialized_len(&self) -> usize { + pub fn serialized_len(&self) -> usize { self.metrics().len } @@ -128,17 +128,15 @@ impl Header { assert!(ext_words <= u8::MAX.into()); let padding_len = (ext_words * 4) - ext_len; let len = BASE_HEADER_LEN + ext_len + padding_len; - HeaderMetrics { - ext_words, - padding_len, - len, - } + HeaderMetrics { ext_words, padding_len, len } } } #[cfg(test)] mod tests { - use super::{*, super::packet::E2ee}; + use crate::dtp::time::Timestamp; + + use super::{super::packet::E2ee, *}; use bytes::Buf; /// Constructed packet to use in tests. @@ -150,13 +148,9 @@ mod tests { track_handle: 0x8811u32.try_into().unwrap(), sequence: 0x4422, frame_number: 0x4411, - timestamp: 0x44221188, + timestamp: Timestamp::from_ticks(0x44221188), user_timestamp: 0x4411221111118811.into(), - e2ee: E2ee { - key_index: 0xFA, - iv: [0x3C; 12], - } - .into(), + e2ee: E2ee { key_index: 0xFA, iv: [0x3C; 12] }.into(), }, payload: vec![0xFA; 1024].into(), } diff --git a/livekit-datatrack/src/dtp/time.rs b/livekit-datatrack/src/dtp/time.rs new file mode 100644 index 000000000..67503fdc0 --- /dev/null +++ b/livekit-datatrack/src/dtp/time.rs @@ -0,0 +1,116 @@ +// Copyright 2025 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 rand::Rng; +use std::time::{Duration, Instant}; + +/// Packet-level timestamp. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Timestamp(u32); + +impl Timestamp { + pub fn random() -> Self { + Self::from_ticks(rand::rng().random::()) + } + + pub const fn from_ticks(ticks: u32) -> Self { + Self(ticks) + } + + pub const fn as_ticks(&self) -> u32 { + self.0 + } + + const fn is_before(&self, other: Self) -> bool { + (self.0.wrapping_sub(other.0) as i32) < 0 + } + + const fn wrapping_add(self, ticks: u32) -> Self { + Self(self.0.wrapping_add(ticks)) + } +} + +/// Monotonic mapping from an epoch to a packet-level timestamp. +#[derive(Debug)] +pub struct Clock { + epoch: Instant, + base: Timestamp, + prev: Timestamp, +} + +impl Clock { + /// Creates a new clock with epoch equal to [`Instant::now()`]. + pub fn new(base: Timestamp) -> Self { + Self::with_epoch(Instant::now(), base) + } + + /// Creates a new clock with an explicit epoch instant. + pub fn with_epoch(epoch: Instant, base: Timestamp) -> Self { + Self { epoch, base, prev: base } + } + + /// Returns the timestamp corresponding to [`Instant::now()`]. + pub fn now(&mut self) -> Timestamp { + self.at(Instant::now()) + } + + /// Returns the timestamp corresponding to the given instant. + pub fn at(&mut self, instant: Instant) -> Timestamp { + let elapsed = instant.duration_since(self.epoch); + let ticks = Self::duration_to_ticks(elapsed); + + let mut ts = self.base.wrapping_add(ticks); + // Enforce monotonicity in RTP wraparound space + if ts.is_before(self.prev) { + ts = self.prev; + } + self.prev = ts; + ts + } + + /// Convert a duration since the epoch into clock ticks. + const fn duration_to_ticks(duration: Duration) -> u32 { + // round(nanos * rate_hz / 1e9) + let nanos = duration.as_nanos(); + let ticks = (nanos * RATE as u128 + 500_000_000) / 1_000_000_000; + ticks as u32 + } +} + +#[cfg(test)] +mod tests { + use super::*; + type DefaultClock = Clock<90_000>; + + #[test] + fn test_is_base_at_epoch() { + let epoch = Instant::now(); + let base = Timestamp::from_ticks(1234); + let mut clock = DefaultClock::with_epoch(epoch, base); + + assert_eq!(clock.at(epoch).as_ticks(), base.as_ticks()); + assert_eq!(clock.prev.as_ticks(), base.as_ticks()); + } + + #[test] + fn test_monotonic() { + let epoch = Instant::now(); + let base = Timestamp::from_ticks(0); + let mut clock = DefaultClock::with_epoch(epoch, base); + + let t1 = epoch + Duration::from_millis(100); + let t0 = epoch + Duration::from_millis(50); + assert_eq!(clock.at(t1).as_ticks(), clock.at(t0).as_ticks(), "Clock went backwards"); + } +} diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index 47e0d9761..52ba5ecd9 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -16,4 +16,5 @@ mod dtp; pub mod error; pub mod frame; pub mod manager; -pub mod track; \ No newline at end of file +pub mod track; +mod utils; \ No newline at end of file diff --git a/livekit-datatrack/src/manager/publish.rs b/livekit-datatrack/src/manager/publish.rs index a85653f35..a74a8ae2d 100644 --- a/livekit-datatrack/src/manager/publish.rs +++ b/livekit-datatrack/src/manager/publish.rs @@ -124,7 +124,8 @@ impl TrackPubTask { e2ee, user_timestamp: frame.user_timestamp, }; - for packet in self.packetizer.packetize(frame) { + let packets = self.packetizer.packetize(frame).context("Failed to packetize frame")?; + for packet in packets { let serialized = packet.serialize(); self.packet_out_tx.try_send(serialized).context("Failed to send packet")?; } diff --git a/livekit-datatrack/src/utils/bytes.rs b/livekit-datatrack/src/utils/bytes.rs new file mode 100644 index 000000000..610e6b4fa --- /dev/null +++ b/livekit-datatrack/src/utils/bytes.rs @@ -0,0 +1,89 @@ +// Copyright 2025 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 bytes::Bytes; + +/// Extension methods for chunking [`Bytes`] into zero-copy payloads. +pub trait BytesChunkExt { + /// Split into zero-copy chunks of size <= `max_size`. + /// + /// # Panics + /// If `max_size` is equal to zero. + /// + fn into_chunks(self, max_size: usize) -> ChunkIter; +} + +impl BytesChunkExt for Bytes { + fn into_chunks(self, max_size: usize) -> ChunkIter { + assert_ne!(max_size, 0, "Zero chunk size is invalid"); + ChunkIter { source: self, max_size } + } +} + +/// An iterator over chunks of a certain size. +/// +/// Internally, this uses [`Bytes::split_to`], an O(1) operation. +/// +pub struct ChunkIter { + source: Bytes, + max_size: usize, +} + +impl Iterator for ChunkIter { + type Item = Bytes; + + fn next(&mut self) -> Option { + if self.source.is_empty() { + return None; + } + let n = self.max_size.min(self.source.len()); + Some(self.source.split_to(n)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[test] + fn test_empty_source() { + let source = Bytes::new(); + let chunks: Vec<_> = source.into_chunks(256).collect(); + assert!(chunks.is_empty()) + } + + #[rstest] + fn test_chunks( + #[values(1, 128, 333)] chunk_size: usize, + #[values(1, 64, 128, 256, 123)] source_size: usize, + ) { + let source = Bytes::from(vec![0xCC; source_size]); + let chunks: Vec<_> = source.into_chunks(chunk_size).collect(); + + let expected_chunks = (source_size + chunk_size - 1) / chunk_size; + assert_eq!(chunks.len(), expected_chunks); + + // All but last chunk's length match chunks size + assert!(chunks[..chunks.len().saturating_sub(1)].iter().all(|c| c.len() == chunk_size)); + + // Last is either full (divisible) or the remainder. + let expected_last_len = if source_size % chunk_size == 0 { + chunk_size.min(source_size) + } else { + source_size % chunk_size + }; + assert_eq!(chunks.last().unwrap().len(), expected_last_len); + } +} diff --git a/livekit-datatrack/src/utils/counter.rs b/livekit-datatrack/src/utils/counter.rs new file mode 100644 index 000000000..09e0ae003 --- /dev/null +++ b/livekit-datatrack/src/utils/counter.rs @@ -0,0 +1,39 @@ +/// A counter that increases monotonically and wraps on overflow. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +pub struct Counter(T); + +#[allow(dead_code)] +impl Counter { + pub fn new(start: T) -> Self { + Self(start) + } + + /// Returns the current value. + pub fn get(self) -> T { + self.0 + } + + /// Returns current value, then increments with wrap-around. + pub fn get_then_increment(&mut self) -> T { + let current = self.0; + self.0 = self.0.wrapping_inc(); + current + } +} + +/// A type that supports incrementing with wrap-around. +pub trait WrappingIncrement: Copy { + fn wrapping_inc(self) -> Self; +} + +macro_rules! impl_increment { + ($($t:ty),* $(,)?) => { + $(impl WrappingIncrement for $t { + fn wrapping_inc(self) -> Self { + self.wrapping_add(1) + } + })* + }; +} + +impl_increment!(u8, u16, u32, u64); \ No newline at end of file diff --git a/livekit-datatrack/src/utils/mod.rs b/livekit-datatrack/src/utils/mod.rs new file mode 100644 index 000000000..9876ddb1d --- /dev/null +++ b/livekit-datatrack/src/utils/mod.rs @@ -0,0 +1,21 @@ +// Copyright 2025 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. + +/// Utilities for working with [`Bytes::bytes`]. +mod bytes; + +mod counter; + +pub use bytes::*; +pub use counter::*; \ No newline at end of file diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index d3ccffd2a..baef7578d 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -1204,6 +1204,8 @@ impl RoomSession { publish_tracks: self.local_participant.published_tracks_info(), data_channels: dcs, datachannel_receive_states: session.data_channel_receive_states(), + publish_data_tracks: vec![], + // TODO: handle sync state }; log::debug!("sending sync state {:?}", sync_state); From b195d5c1d0bf0df109b824c985f71b744b974a3a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 20 Dec 2025 15:23:30 +1100 Subject: [PATCH 030/232] Hide version field --- livekit-datatrack/src/dtp/deserialize.rs | 2 -- livekit-datatrack/src/dtp/packet.rs | 1 - livekit-datatrack/src/dtp/packetizer.rs | 1 - livekit-datatrack/src/dtp/serialize.rs | 1 - 4 files changed, 5 deletions(-) diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs index d850bdfc4..20c69022c 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -85,7 +85,6 @@ impl Header { let extensions = Extensions::parse(ext_block)?; let header = Header { - version, is_final, track_handle, sequence, @@ -190,7 +189,6 @@ mod tests { raw.put_slice(&[0x44, 0x22, 0x11, 0x88]); // Timestamp let dtp = Dtp::deserialize(raw.freeze()).unwrap(); - assert_eq!(dtp.header.version, 0); assert_eq!(dtp.header.is_final, true); assert_eq!(dtp.header.track_handle, 0x8811u32.try_into().unwrap()); assert_eq!(dtp.header.sequence, 0x4422); diff --git a/livekit-datatrack/src/dtp/packet.rs b/livekit-datatrack/src/dtp/packet.rs index 503f993a1..ba5aa53ca 100644 --- a/livekit-datatrack/src/dtp/packet.rs +++ b/livekit-datatrack/src/dtp/packet.rs @@ -26,7 +26,6 @@ pub struct Dtp { #[derive(Debug, Clone)] pub struct Header { - pub version: u8, pub is_final: bool, pub track_handle: TrackHandle, pub sequence: u16, diff --git a/livekit-datatrack/src/dtp/packetizer.rs b/livekit-datatrack/src/dtp/packetizer.rs index 11f56fe2a..c6c729b9d 100644 --- a/livekit-datatrack/src/dtp/packetizer.rs +++ b/livekit-datatrack/src/dtp/packetizer.rs @@ -61,7 +61,6 @@ impl Packetizer { pub fn packetize(&mut self, frame: PacketizerFrame) -> Result, PacketizerError> { // TODO: consider using default let header = Header { - version: 0, is_final: false, track_handle: self.track_handle, sequence: 0, diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/dtp/serialize.rs index 6b4d8be2a..103f0d6d2 100644 --- a/livekit-datatrack/src/dtp/serialize.rs +++ b/livekit-datatrack/src/dtp/serialize.rs @@ -143,7 +143,6 @@ mod tests { fn packet() -> Dtp { Dtp { header: Header { - version: 0, is_final: true, track_handle: 0x8811u32.try_into().unwrap(), sequence: 0x4422, From 32de6214c7412d9e44d567b6adef849dfa29c8d1 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 20 Dec 2025 15:41:14 +1100 Subject: [PATCH 031/232] Rename --- livekit-datatrack/src/manager/publish.rs | 9 +++++---- livekit-datatrack/src/track.rs | 16 ++++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/livekit-datatrack/src/manager/publish.rs b/livekit-datatrack/src/manager/publish.rs index a74a8ae2d..788a3914f 100644 --- a/livekit-datatrack/src/manager/publish.rs +++ b/livekit-datatrack/src/manager/publish.rs @@ -31,6 +31,7 @@ use tokio::{ }; use tokio_stream::wrappers::ReceiverStream; +// TODO: relocate #[derive(Debug, Clone, Copy)] pub enum DataTrackState { Published, @@ -38,12 +39,12 @@ pub enum DataTrackState { } #[derive(Debug, Clone)] -pub(crate) struct PubHandle { +pub(crate) struct LocalDataTrackInner { frame_tx: mpsc::Sender, state_tx: watch::Sender, } -impl PubHandle { +impl LocalDataTrackInner { pub fn publish(&self, frame: DataTrackFrame) -> Result<(), PublishFrameError> { if !self.is_published() { return Err(PublishFrameError::new(frame, PublishFrameErrorReason::TrackUnpublished)); @@ -65,7 +66,7 @@ impl PubHandle { } } -impl Drop for PubHandle { +impl Drop for LocalDataTrackInner { fn drop(&mut self) { // Implicit unpublish when handle dropped. self.unpublish(); @@ -291,7 +292,7 @@ impl PubManagerTask { livekit_runtime::spawn(task.run()); self.active_publications.insert(info.handle, state_tx.clone()); - let handle = PubHandle { frame_tx, state_tx }; + let handle = LocalDataTrackInner { frame_tx, state_tx }; DataTrack::new(info, handle) } diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index 67069b079..37952205c 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -82,8 +82,8 @@ pub struct DataTrack { #[derive(Debug, Clone, FromVariants)] enum DataTrackInner { - Local(manager::PubHandle), - Remote(()), // TODO: add sub handle + Local(manager::LocalDataTrackInner), + Remote(()), // TODO: add sub inner } impl DataTrack { @@ -94,11 +94,11 @@ impl DataTrack { } impl DataTrack { - pub(crate) fn new(info: Arc, handle: manager::PubHandle) -> Self { - Self { info, inner: handle.into(), _location: PhantomData } + pub(crate) fn new(info: Arc, inner: manager::LocalDataTrackInner) -> Self { + Self { info, inner: inner.into(), _location: PhantomData } } - fn handle(&self) -> &manager::PubHandle { + fn inner(&self) -> &manager::LocalDataTrackInner { match &self.inner { DataTrackInner::Local(publisher) => publisher, DataTrackInner::Remote(_) => unreachable!(), // Safe (type state) @@ -107,17 +107,17 @@ impl DataTrack { /// Publish a frame onto the track. pub fn publish(&self, frame: impl Into) -> Result<(), PublishFrameError> { - self.handle().publish(frame.into()) + self.inner().publish(frame.into()) } /// Whether or not the track is still published. pub fn is_published(&self) -> bool { - self.handle().is_published() + self.inner().is_published() } /// Unpublish the track. pub fn unpublish(self) { - self.handle().unpublish() + self.inner().unpublish() } } From e80218b5ec0e41fea41414a78d72b2dd59794c6d Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 20 Dec 2025 16:52:36 +1100 Subject: [PATCH 032/232] Reorganize modules Divide by functionality instead of type (local vs. remote track) --- .../src/{manager => common}/e2ee.rs | 20 ++- livekit-datatrack/src/{ => common}/frame.rs | 7 +- livekit-datatrack/src/common/mod.rs | 25 ++++ livekit-datatrack/src/common/track.rs | 59 +++++++++ livekit-datatrack/src/dtp/mod.rs | 8 +- livekit-datatrack/src/lib.rs | 12 +- .../{manager/publish.rs => local/manager.rs} | 8 +- .../src/{error.rs => local/mod.rs} | 93 +++++++++---- livekit-datatrack/src/manager/subscribe.rs | 50 ------- livekit-datatrack/src/remote/manager.rs | 47 +++++++ .../src/{manager => remote}/mod.rs | 14 +- livekit-datatrack/src/track.rs | 124 ------------------ livekit-datatrack/src/utils/counter.rs | 16 ++- livekit-datatrack/src/utils/mod.rs | 2 +- livekit/tests/data_track_test.rs | 14 ++ 15 files changed, 261 insertions(+), 238 deletions(-) rename livekit-datatrack/src/{manager => common}/e2ee.rs (84%) rename livekit-datatrack/src/{ => common}/frame.rs (89%) create mode 100644 livekit-datatrack/src/common/mod.rs create mode 100644 livekit-datatrack/src/common/track.rs rename livekit-datatrack/src/{manager/publish.rs => local/manager.rs} (98%) rename livekit-datatrack/src/{error.rs => local/mod.rs} (61%) delete mode 100644 livekit-datatrack/src/manager/subscribe.rs create mode 100644 livekit-datatrack/src/remote/manager.rs rename livekit-datatrack/src/{manager => remote}/mod.rs (77%) delete mode 100644 livekit-datatrack/src/track.rs diff --git a/livekit-datatrack/src/manager/e2ee.rs b/livekit-datatrack/src/common/e2ee.rs similarity index 84% rename from livekit-datatrack/src/manager/e2ee.rs rename to livekit-datatrack/src/common/e2ee.rs index a700c7fe0..9db8e13bf 100644 --- a/livekit-datatrack/src/manager/e2ee.rs +++ b/livekit-datatrack/src/common/e2ee.rs @@ -13,30 +13,26 @@ // limitations under the License. use bytes::Bytes; -use std::fmt::Debug; +use core::fmt::Debug; use thiserror::Error; +#[derive(Debug, Error)] +#[error("End-to-end encryption failed")] +pub struct E2eeError; + pub struct EncryptedPayload { pub payload: Bytes, pub iv: [u8; 12], pub key_index: u8, } -#[derive(Debug, Error)] -#[error("Encryption failed")] -pub struct EncryptionError; - pub trait EncryptionProvider: Send + Sync + Debug { /// Encrypt the given payload. - fn encrypt(&self, payload: Bytes) -> Result; + fn encrypt(&self, payload: Bytes) -> Result; } -#[derive(Debug, Error)] -#[error("Decryption failed")] -pub struct DecryptionError; - pub trait DecryptionProvider: Send + Sync + Debug { /// Decrypt the given payload. - fn decrypt(&self, payload: EncryptedPayload) -> Result; + fn decrypt(&self, payload: EncryptedPayload) -> Result; // TODO: handle publisher identity -} \ No newline at end of file +} diff --git a/livekit-datatrack/src/frame.rs b/livekit-datatrack/src/common/frame.rs similarity index 89% rename from livekit-datatrack/src/frame.rs rename to livekit-datatrack/src/common/frame.rs index fdc4c3349..4fe434a45 100644 --- a/livekit-datatrack/src/frame.rs +++ b/livekit-datatrack/src/common/frame.rs @@ -46,11 +46,8 @@ impl DataTrackFrameBuilder { } pub fn build(self) -> DataTrackFrame { - DataTrackFrame { - payload: self.payload, - user_timestamp: self.user_timestamp - } + DataTrackFrame { payload: self.payload, user_timestamp: self.user_timestamp } } } -// TODO: just show payload length in debug. \ No newline at end of file +// TODO: just show payload length in debug. diff --git a/livekit-datatrack/src/common/mod.rs b/livekit-datatrack/src/common/mod.rs new file mode 100644 index 000000000..90b3e9189 --- /dev/null +++ b/livekit-datatrack/src/common/mod.rs @@ -0,0 +1,25 @@ +// Copyright 2025 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 thiserror::Error; + +pub mod e2ee; +pub mod frame; +pub mod track; + +pub use {e2ee::*, frame::*, track::*}; + +#[derive(Debug, Error)] +#[error(transparent)] +pub struct InternalError(#[from] anyhow::Error); diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs new file mode 100644 index 000000000..41d3b2c6d --- /dev/null +++ b/livekit-datatrack/src/common/track.rs @@ -0,0 +1,59 @@ +// Copyright 2025 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 from_variants::FromVariants; +use std::{marker::PhantomData, sync::Arc}; + +pub use crate::dtp::TrackHandle; + +#[derive(Debug, Clone)] +pub struct DataTrackInfo { + pub(crate) sid: String, // TODO: use shared ID type + pub(crate) handle: TrackHandle, + pub(crate) name: String, + pub(crate) uses_e2ee: bool, +} + +impl DataTrackInfo { + pub fn sid(&self) -> &String { + &self.sid + } + pub fn name(&self) -> &str { + &self.name + } + pub fn uses_e2ee(&self) -> bool { + self.uses_e2ee + } +} + +#[derive(Debug, Clone)] +pub struct DataTrack { + pub(crate) info: Arc, + pub(crate) inner: DataTrackInner, + /// Marker indicating local or remote. + pub(crate) _location: PhantomData, +} + +#[derive(Debug, Clone, FromVariants)] +pub(crate) enum DataTrackInner { + Local(crate::local::manager::LocalDataTrackInner), + Remote(()), // TODO: add sub inner +} + +impl DataTrack { + /// Information about the data track such as name. + pub fn info(&self) -> &DataTrackInfo { + &self.info + } +} diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index cc0b911f3..fc936a67d 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -12,15 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod deserialize; mod packet; mod packetizer; -mod track_handle; -mod deserialize; mod serialize; mod time; +mod track_handle; +pub use deserialize::*; pub use packet::*; pub use packetizer::*; +pub use serialize::*; pub use track_handle::*; -pub use deserialize::*; -pub use serialize::*; \ No newline at end of file diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index 52ba5ecd9..ecb10c60d 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod common; +mod local; +mod remote; + mod dtp; -pub mod error; -pub mod frame; -pub mod manager; -pub mod track; -mod utils; \ No newline at end of file +mod utils; + +pub use {common::*, local::*, remote::*}; diff --git a/livekit-datatrack/src/manager/publish.rs b/livekit-datatrack/src/local/manager.rs similarity index 98% rename from livekit-datatrack/src/manager/publish.rs rename to livekit-datatrack/src/local/manager.rs index 788a3914f..61562fe92 100644 --- a/livekit-datatrack/src/manager/publish.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -12,12 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::Local; use crate::{ - dtp, - error::{InternalError, PublishError, PublishFrameError, PublishFrameErrorReason}, - frame::DataTrackFrame, - manager::e2ee::EncryptionProvider, - track::{DataTrack, DataTrackInfo, DataTrackOptions, Local, TrackHandle}, + dtp, DataTrack, DataTrackFrame, DataTrackInfo, DataTrackOptions, EncryptionProvider, + InternalError, PublishError, PublishFrameError, PublishFrameErrorReason, TrackHandle, }; use anyhow::{anyhow, Context}; use bytes::Bytes; diff --git a/livekit-datatrack/src/error.rs b/livekit-datatrack/src/local/mod.rs similarity index 61% rename from livekit-datatrack/src/error.rs rename to livekit-datatrack/src/local/mod.rs index 414b4d751..7f3d934de 100644 --- a/livekit-datatrack/src/error.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -12,10 +12,76 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::frame::DataTrackFrame; -use core::fmt; +use crate::{DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, InternalError}; +use std::{fmt, marker::PhantomData, sync::Arc}; use thiserror::Error; +pub(crate) mod manager; + +/// Marker type indicating a [`DataTrack`] belongs to the local participant. +#[derive(Debug)] +pub struct Local; + +impl DataTrack { + pub(crate) fn new(info: Arc, inner: manager::LocalDataTrackInner) -> Self { + Self { info, inner: inner.into(), _location: PhantomData } + } + + fn inner(&self) -> &manager::LocalDataTrackInner { + match &self.inner { + DataTrackInner::Local(publisher) => publisher, + DataTrackInner::Remote(_) => unreachable!(), // Safe (type state) + } + } + + /// Publish a frame onto the track. + pub fn publish(&self, frame: impl Into) -> Result<(), PublishFrameError> { + self.inner().publish(frame.into()) + } + + /// Whether or not the track is still published. + pub fn is_published(&self) -> bool { + self.inner().is_published() + } + + /// Unpublish the track. + pub fn unpublish(self) { + self.inner().unpublish() + } +} + +impl PublishFrameError { + pub(crate) fn new(frame: DataTrackFrame, reason: PublishFrameErrorReason) -> Self { + Self { frame, reason } + } + + /// Consume the error, returning the frame that couldn't be published. + pub fn into_frame(self) -> DataTrackFrame { + self.frame + } + + /// Returns the reason why the frame could not be published. + pub fn reason(&self) -> PublishFrameErrorReason { + self.reason + } +} + +/// Options for publishing a data track. +#[derive(Clone, Debug)] +pub struct DataTrackOptions { + pub(crate) name: String, + pub(crate) disable_e2ee: bool, +} + +impl DataTrackOptions { + pub fn with_name(name: impl Into) -> Self { + Self { name: name.into(), disable_e2ee: false } + } + pub fn disable_e2ee(self, disabled: bool) -> Self { + Self { disable_e2ee: disabled, ..self } + } +} + #[derive(Debug, Error)] pub enum PublishError { #[error("The local participant does not have permission to publish data tracks")] @@ -32,9 +98,6 @@ pub enum PublishError { Internal(#[from] InternalError), } -#[derive(Debug, Error)] -pub enum SubscribeError {} - /// An error that can occur when publishing a frame onto a data track. #[derive(Debug, Error)] #[error("Failed to publish frame: {reason}")] @@ -43,22 +106,6 @@ pub struct PublishFrameError { reason: PublishFrameErrorReason, } -impl PublishFrameError { - pub(crate) fn new(frame: DataTrackFrame, reason: PublishFrameErrorReason) -> Self { - Self { frame, reason } - } - - /// Consume the error, returning the frame that couldn't be published. - pub fn into_frame(self) -> DataTrackFrame { - self.frame - } - - /// Returns the reason why the frame could not be published. - pub fn reason(&self) -> PublishFrameErrorReason { - self.reason - } -} - /// Reason why a data track frame could not be published. #[derive(Debug, Clone, Copy)] pub enum PublishFrameErrorReason { @@ -78,7 +125,3 @@ impl fmt::Display for PublishFrameErrorReason { } } } - -#[derive(Debug, Error)] -#[error(transparent)] -pub struct InternalError(#[from] anyhow::Error); diff --git a/livekit-datatrack/src/manager/subscribe.rs b/livekit-datatrack/src/manager/subscribe.rs deleted file mode 100644 index 23370277c..000000000 --- a/livekit-datatrack/src/manager/subscribe.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2025 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 dashmap::DashMap; -use super::TrackHandle; - -#[derive(Debug)] -pub struct SubManagerOptions { - // Dependencies - // - E2EE - // - Signaling - // - Tx UpdateDataSubscription - // - Rx DataTrackPublishedResponse, DataTrackUnpublishedResponse, - // - Data track channel - // - Rx -} - -#[derive(Debug)] -pub struct SubManager { - options: SubManagerOptions, - sub_tracks: DashMap -} - -impl SubManager { - pub fn new(options: SubManagerOptions) -> Self { - Self { options, sub_tracks: DashMap::default() } - } -} - -// handles: track published/unpublished -// maintains state -// creates DataTrack when track published -// send signal to existing ones when track unpublished - -#[derive(Debug)] -pub struct Descriptor { - // tx - // data channel -> dtp decode -> frame -> tx -} \ No newline at end of file diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs new file mode 100644 index 000000000..e40df4cfa --- /dev/null +++ b/livekit-datatrack/src/remote/manager.rs @@ -0,0 +1,47 @@ +// Copyright 2025 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. + +// #[derive(Debug)] +// pub struct SubManagerOptions { +// // Dependencies +// // - E2EE +// // - Signaling +// // - Tx UpdateDataSubscription +// // - Rx DataTrackPublishedResponse, DataTrackUnpublishedResponse, +// // - Data track channel +// // - Rx +// } + +// #[derive(Debug)] +// pub struct SubManager { +// options: SubManagerOptions, +// sub_tracks: DashMap +// } + +// impl SubManager { +// pub fn new(options: SubManagerOptions) -> Self { +// Self { options, sub_tracks: DashMap::default() } +// } +// } + +// // handles: track published/unpublished +// // maintains state +// // creates DataTrack when track published +// // send signal to existing ones when track unpublished + +// #[derive(Debug)] +// pub struct Descriptor { +// // tx +// // data channel -> dtp decode -> frame -> tx +// } \ No newline at end of file diff --git a/livekit-datatrack/src/manager/mod.rs b/livekit-datatrack/src/remote/mod.rs similarity index 77% rename from livekit-datatrack/src/manager/mod.rs rename to livekit-datatrack/src/remote/mod.rs index 13fab6b18..029126a22 100644 --- a/livekit-datatrack/src/manager/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -12,10 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod e2ee; -pub mod publish; -// pub mod subscribe; +use crate::DataTrack; -pub use e2ee::*; -pub use publish::*; -// pub use subscribe::*; \ No newline at end of file +mod manager; + +/// Marker type indicating a [`DataTrack`] belongs to a remote participant. +#[derive(Debug)] +pub struct Remote; + +impl DataTrack {} diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs deleted file mode 100644 index 37952205c..000000000 --- a/livekit-datatrack/src/track.rs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2025 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 crate::{ - error::PublishFrameError, - frame::DataTrackFrame, - manager::{self}, -}; -use from_variants::FromVariants; -use std::{marker::PhantomData, sync::Arc}; - -pub use crate::dtp::TrackHandle; - -/// Options for publishing a data track. -#[derive(Clone, Debug)] -pub struct DataTrackOptions { - pub(crate) name: String, - pub(crate) disable_e2ee: bool, -} - -impl DataTrackOptions { - pub fn with_name(name: impl Into) -> Self { - Self { name: name.into(), disable_e2ee: false } - } - pub fn disable_e2ee(self, disabled: bool) -> Self { - Self { disable_e2ee: disabled, ..self } - } -} - -#[derive(Debug, Clone)] -pub struct DataTrackInfo { - pub(crate) sid: String, // TODO: use shared ID type - pub(crate) handle: TrackHandle, - pub(crate) name: String, - pub(crate) uses_e2ee: bool, -} - -impl DataTrackInfo { - pub fn sid(&self) -> &String { - &self.sid - } - pub fn name(&self) -> &str { - &self.name - } - pub fn uses_e2ee(&self) -> bool { - self.uses_e2ee - } -} - -/// Marker type indicating a [`DataTrack`] belongs to the local participant. -#[derive(Debug)] -pub struct Local; - -/// Marker type indicating a [`DataTrack`] belongs to a remote participant. -#[derive(Debug)] -pub struct Remote; - -/// A data track published by the local participant. -pub type LocalDataTrack = DataTrack; - -/// A data track published by a remote participant. -pub type RemoteDataTrack = DataTrack; - -#[derive(Debug, Clone)] -pub struct DataTrack { - info: Arc, - inner: DataTrackInner, - /// Marker indicating local or remote. - _location: PhantomData, -} - -#[derive(Debug, Clone, FromVariants)] -enum DataTrackInner { - Local(manager::LocalDataTrackInner), - Remote(()), // TODO: add sub inner -} - -impl DataTrack { - /// Information about the data track such as name. - pub fn info(&self) -> &DataTrackInfo { - &self.info - } -} - -impl DataTrack { - pub(crate) fn new(info: Arc, inner: manager::LocalDataTrackInner) -> Self { - Self { info, inner: inner.into(), _location: PhantomData } - } - - fn inner(&self) -> &manager::LocalDataTrackInner { - match &self.inner { - DataTrackInner::Local(publisher) => publisher, - DataTrackInner::Remote(_) => unreachable!(), // Safe (type state) - } - } - - /// Publish a frame onto the track. - pub fn publish(&self, frame: impl Into) -> Result<(), PublishFrameError> { - self.inner().publish(frame.into()) - } - - /// Whether or not the track is still published. - pub fn is_published(&self) -> bool { - self.inner().is_published() - } - - /// Unpublish the track. - pub fn unpublish(self) { - self.inner().unpublish() - } -} - -// TODO: implement remote track (subscriber) \ No newline at end of file diff --git a/livekit-datatrack/src/utils/counter.rs b/livekit-datatrack/src/utils/counter.rs index 09e0ae003..be934a7af 100644 --- a/livekit-datatrack/src/utils/counter.rs +++ b/livekit-datatrack/src/utils/counter.rs @@ -1,3 +1,17 @@ +// Copyright 2025 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. + /// A counter that increases monotonically and wraps on overflow. #[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] pub struct Counter(T); @@ -36,4 +50,4 @@ macro_rules! impl_increment { }; } -impl_increment!(u8, u16, u32, u64); \ No newline at end of file +impl_increment!(u8, u16, u32, u64); diff --git a/livekit-datatrack/src/utils/mod.rs b/livekit-datatrack/src/utils/mod.rs index 9876ddb1d..de7957d22 100644 --- a/livekit-datatrack/src/utils/mod.rs +++ b/livekit-datatrack/src/utils/mod.rs @@ -18,4 +18,4 @@ mod bytes; mod counter; pub use bytes::*; -pub use counter::*; \ No newline at end of file +pub use counter::*; diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 438477e22..7168ed2d4 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -1,3 +1,17 @@ +// Copyright 2025 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}, From 45faf39a957f5fcd9c91d27dd13698dd5b665189 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 20 Dec 2025 16:59:11 +1100 Subject: [PATCH 033/232] Include type aliases --- livekit-datatrack/src/local/mod.rs | 3 +++ livekit-datatrack/src/remote/mod.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 7f3d934de..2be8ec24e 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -18,6 +18,9 @@ use thiserror::Error; pub(crate) mod manager; +/// Data track published by the local participant. +pub type LocalDataTrack = DataTrack; + /// Marker type indicating a [`DataTrack`] belongs to the local participant. #[derive(Debug)] pub struct Local; diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 029126a22..499347a9a 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -16,6 +16,9 @@ use crate::DataTrack; mod manager; +/// Data track published by a remote participant. +pub type RemoteDataTrack = DataTrack; + /// Marker type indicating a [`DataTrack`] belongs to a remote participant. #[derive(Debug)] pub struct Remote; From 965a65f4f587f0882e85373dda872c1152a2b235 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 20 Dec 2025 17:14:02 +1100 Subject: [PATCH 034/232] Renaming --- livekit-datatrack/src/common/track.rs | 2 +- livekit-datatrack/src/local/manager.rs | 37 +++++++++++++------------- livekit-datatrack/src/local/mod.rs | 4 +-- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs index 41d3b2c6d..5b7f5c099 100644 --- a/livekit-datatrack/src/common/track.rs +++ b/livekit-datatrack/src/common/track.rs @@ -47,7 +47,7 @@ pub struct DataTrack { #[derive(Debug, Clone, FromVariants)] pub(crate) enum DataTrackInner { - Local(crate::local::manager::LocalDataTrackInner), + Local(crate::local::manager::TrackInner), Remote(()), // TODO: add sub inner } diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 61562fe92..7ea8af5fc 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -37,12 +37,12 @@ pub enum DataTrackState { } #[derive(Debug, Clone)] -pub(crate) struct LocalDataTrackInner { +pub(crate) struct TrackInner { frame_tx: mpsc::Sender, state_tx: watch::Sender, } -impl LocalDataTrackInner { +impl TrackInner { pub fn publish(&self, frame: DataTrackFrame) -> Result<(), PublishFrameError> { if !self.is_published() { return Err(PublishFrameError::new(frame, PublishFrameErrorReason::TrackUnpublished)); @@ -64,14 +64,15 @@ impl LocalDataTrackInner { } } -impl Drop for LocalDataTrackInner { +impl Drop for TrackInner { fn drop(&mut self) { // Implicit unpublish when handle dropped. self.unpublish(); } } -struct TrackPubTask { +/// Task responsible for operating an individual published data track. +struct TrackTask { // TODO: packetizer, e2ee_provider, rate tracking, etc. packetizer: dtp::Packetizer, encryption: Option>, @@ -82,7 +83,7 @@ struct TrackPubTask { signal_out_tx: mpsc::Sender, } -impl TrackPubTask { +impl TrackTask { async fn run(mut self) -> Result<(), InternalError> { let mut state = DataTrackState::Published; while matches!(state, DataTrackState::Published) { @@ -144,18 +145,18 @@ pub struct PubManagerOptions { /// Manager for data track publications. #[derive(Debug, Clone)] -pub struct PubManager { +pub struct Manager { signal_in_tx: mpsc::Sender, pub_req_tx: mpsc::Sender, } -impl PubManager { +impl Manager { const CH_BUFFER_SIZE: usize = 4; const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); pub fn new( options: PubManagerOptions, - ) -> (Self, PubManagerTask, impl Stream, impl Stream) + ) -> (Self, ManagerTask, impl Stream, impl Stream) { let (pub_req_tx, pub_req_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); let (signal_in_tx, signal_in_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); @@ -163,7 +164,7 @@ impl PubManager { let (packet_out_tx, packet_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); let manager = Self { signal_in_tx, pub_req_tx }; - let task = PubManagerTask { + let task = ManagerTask { encryption: options.encryption, pub_req_rx, signal_in_rx, @@ -207,7 +208,12 @@ impl PubManager { } } -pub struct PubManagerTask { +struct PubRequest { + options: DataTrackOptions, + result_tx: oneshot::Sender, PublishError>>, +} + +pub struct ManagerTask { encryption: Option>, pub_req_rx: mpsc::Receiver, signal_in_rx: mpsc::Receiver, @@ -219,7 +225,7 @@ pub struct PubManagerTask { active_publications: HashMap>, } -impl PubManagerTask { +impl ManagerTask { pub async fn run(mut self) -> Result<(), InternalError> { loop { tokio::select! { @@ -277,7 +283,7 @@ impl PubManagerTask { let (state_tx, state_rx) = watch::channel(DataTrackState::Published); let info = Arc::new(info); - let task = TrackPubTask { + let task = TrackTask { // TODO: handle cancellation packetizer: dtp::Packetizer::new(info.handle, 16_000), encryption: self.encryption.clone(), @@ -290,7 +296,7 @@ impl PubManagerTask { livekit_runtime::spawn(task.run()); self.active_publications.insert(info.handle, state_tx.clone()); - let handle = LocalDataTrackInner { frame_tx, state_tx }; + let handle = TrackInner { frame_tx, state_tx }; DataTrack::new(info, handle) } @@ -348,11 +354,6 @@ impl PubManagerTask { } } -struct PubRequest { - options: DataTrackOptions, - result_tx: oneshot::Sender, PublishError>>, -} - #[derive(Debug, FromVariants)] pub enum PubManagerInput { Signal(PubSignalInput), diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 2be8ec24e..dfc3633ff 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -26,11 +26,11 @@ pub type LocalDataTrack = DataTrack; pub struct Local; impl DataTrack { - pub(crate) fn new(info: Arc, inner: manager::LocalDataTrackInner) -> Self { + pub(crate) fn new(info: Arc, inner: manager::TrackInner) -> Self { Self { info, inner: inner.into(), _location: PhantomData } } - fn inner(&self) -> &manager::LocalDataTrackInner { + fn inner(&self) -> &manager::TrackInner { match &self.inner { DataTrackInner::Local(publisher) => publisher, DataTrackInner::Remote(_) => unreachable!(), // Safe (type state) From 2c60863ab704a034465353bc11bbf4b005563007 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 20 Dec 2025 17:27:45 +1100 Subject: [PATCH 035/232] Documentation --- livekit-datatrack/src/common/track.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs index 5b7f5c099..aebf1afdd 100644 --- a/livekit-datatrack/src/common/track.rs +++ b/livekit-datatrack/src/common/track.rs @@ -17,6 +17,7 @@ use std::{marker::PhantomData, sync::Arc}; pub use crate::dtp::TrackHandle; +/// Information about a data track. #[derive(Debug, Clone)] pub struct DataTrackInfo { pub(crate) sid: String, // TODO: use shared ID type @@ -26,12 +27,15 @@ pub struct DataTrackInfo { } impl DataTrackInfo { - pub fn sid(&self) -> &String { + /// Unique track identifier. + pub fn sid(&self) -> &str { &self.sid } + /// Name of the track assigned when published. pub fn name(&self) -> &str { &self.name } + /// Whether or not frames sent on the track use end-to-end encryption. pub fn uses_e2ee(&self) -> bool { self.uses_e2ee } From 5637b28c2168c8b0928c9308c293889f043bed17 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 20 Dec 2025 17:30:03 +1100 Subject: [PATCH 036/232] Do not expose internal type --- livekit-datatrack/src/common/track.rs | 3 +-- livekit-datatrack/src/local/manager.rs | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs index aebf1afdd..597d143ed 100644 --- a/livekit-datatrack/src/common/track.rs +++ b/livekit-datatrack/src/common/track.rs @@ -14,8 +14,7 @@ use from_variants::FromVariants; use std::{marker::PhantomData, sync::Arc}; - -pub use crate::dtp::TrackHandle; +use crate::dtp::TrackHandle; /// Information about a data track. #[derive(Debug, Clone)] diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 7ea8af5fc..99e3920ae 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -15,8 +15,9 @@ use super::Local; use crate::{ dtp, DataTrack, DataTrackFrame, DataTrackInfo, DataTrackOptions, EncryptionProvider, - InternalError, PublishError, PublishFrameError, PublishFrameErrorReason, TrackHandle, + InternalError, PublishError, PublishFrameError, PublishFrameErrorReason, }; +use crate::dtp::TrackHandle; use anyhow::{anyhow, Context}; use bytes::Bytes; use from_variants::FromVariants; From 65a81c64e841e2d02d3a0dc8b6c714557c965418 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:40:07 +1100 Subject: [PATCH 037/232] Docs --- livekit-datatrack/src/common/frame.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/livekit-datatrack/src/common/frame.rs b/livekit-datatrack/src/common/frame.rs index 4fe434a45..00c030deb 100644 --- a/livekit-datatrack/src/common/frame.rs +++ b/livekit-datatrack/src/common/frame.rs @@ -14,6 +14,10 @@ use bytes::Bytes; +/// Frame published on a data track containing metadata and a payload. +/// +/// Construct using [`DataTrackFrameBuilder`]. +/// #[derive(Debug)] pub struct DataTrackFrame { pub(crate) payload: Bytes, @@ -21,14 +25,18 @@ pub struct DataTrackFrame { } impl DataTrackFrame { + /// Get the frame's payload. pub fn payload(&self) -> Bytes { self.payload.clone() // Cheap clone } + + /// Get the frame's user timestamp, if attached. pub fn user_timestamp(&self) -> Option { self.user_timestamp } } +/// Constructs a [`DataTrackFrame`]. #[derive(Default)] pub struct DataTrackFrameBuilder { payload: Bytes, From ffcd696366a1e81ad73b146b1994d72f9346bffc Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:43:03 +1100 Subject: [PATCH 038/232] Relocate type --- livekit-datatrack/src/common/track.rs | 6 ++++++ livekit-datatrack/src/local/manager.rs | 16 ++++------------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs index 597d143ed..9d3876654 100644 --- a/livekit-datatrack/src/common/track.rs +++ b/livekit-datatrack/src/common/track.rs @@ -25,6 +25,12 @@ pub struct DataTrackInfo { pub(crate) uses_e2ee: bool, } +#[derive(Debug, Clone, Copy)] +pub(crate) enum DataTrackState { + Published, + Unpublished { sfu_initiated: bool }, +} + impl DataTrackInfo { /// Unique track identifier. pub fn sid(&self) -> &str { diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 99e3920ae..716e82228 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -13,11 +13,11 @@ // limitations under the License. use super::Local; +use crate::dtp::TrackHandle; use crate::{ - dtp, DataTrack, DataTrackFrame, DataTrackInfo, DataTrackOptions, EncryptionProvider, - InternalError, PublishError, PublishFrameError, PublishFrameErrorReason, + dtp, DataTrack, DataTrackFrame, DataTrackInfo, DataTrackOptions, DataTrackState, + EncryptionProvider, InternalError, PublishError, PublishFrameError, PublishFrameErrorReason, }; -use crate::dtp::TrackHandle; use anyhow::{anyhow, Context}; use bytes::Bytes; use from_variants::FromVariants; @@ -30,13 +30,6 @@ use tokio::{ }; use tokio_stream::wrappers::ReceiverStream; -// TODO: relocate -#[derive(Debug, Clone, Copy)] -pub enum DataTrackState { - Published, - Unpublished { sfu_initiated: bool }, -} - #[derive(Debug, Clone)] pub(crate) struct TrackInner { frame_tx: mpsc::Sender, @@ -157,8 +150,7 @@ impl Manager { pub fn new( options: PubManagerOptions, - ) -> (Self, ManagerTask, impl Stream, impl Stream) - { + ) -> (Self, ManagerTask, impl Stream, impl Stream) { let (pub_req_tx, pub_req_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); let (signal_in_tx, signal_in_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); let (signal_out_tx, signal_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); From 98dd4293cffb1e5be55d9e482153688a0c5d4abf Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:49:46 +1100 Subject: [PATCH 039/232] Track inner for remote track --- livekit-datatrack/src/common/track.rs | 2 +- livekit-datatrack/src/local/manager.rs | 2 +- livekit-datatrack/src/local/mod.rs | 2 +- livekit-datatrack/src/remote/manager.rs | 3 +++ livekit-datatrack/src/remote/mod.rs | 18 +++++++++++++++--- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs index 9d3876654..3309ac018 100644 --- a/livekit-datatrack/src/common/track.rs +++ b/livekit-datatrack/src/common/track.rs @@ -57,7 +57,7 @@ pub struct DataTrack { #[derive(Debug, Clone, FromVariants)] pub(crate) enum DataTrackInner { Local(crate::local::manager::TrackInner), - Remote(()), // TODO: add sub inner + Remote(crate::remote::manager::TrackInner), } impl DataTrack { diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 716e82228..1d771a90e 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -290,7 +290,7 @@ impl ManagerTask { self.active_publications.insert(info.handle, state_tx.clone()); let handle = TrackInner { frame_tx, state_tx }; - DataTrack::new(info, handle) + DataTrack::::new(info, handle) } fn handle_request_response( diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index dfc3633ff..eb60f7cea 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -32,7 +32,7 @@ impl DataTrack { fn inner(&self) -> &manager::TrackInner { match &self.inner { - DataTrackInner::Local(publisher) => publisher, + DataTrackInner::Local(track) => track, DataTrackInner::Remote(_) => unreachable!(), // Safe (type state) } } diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index e40df4cfa..aaeff606a 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[derive(Debug, Clone)] +pub(crate) struct TrackInner {} + // #[derive(Debug)] // pub struct SubManagerOptions { // // Dependencies diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 499347a9a..ccaf8d76f 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::DataTrack; +use crate::{DataTrack, DataTrackInfo, DataTrackInner}; +use std::{marker::PhantomData, sync::Arc}; -mod manager; +pub(crate) mod manager; /// Data track published by a remote participant. pub type RemoteDataTrack = DataTrack; @@ -23,4 +24,15 @@ pub type RemoteDataTrack = DataTrack; #[derive(Debug)] pub struct Remote; -impl DataTrack {} +impl DataTrack { + pub(crate) fn new(info: Arc, inner: manager::TrackInner) -> Self { + Self { info, inner: inner.into(), _location: PhantomData } + } + + fn inner(&self) -> &manager::TrackInner { + match &self.inner { + DataTrackInner::Remote(inner) => inner, + DataTrackInner::Local(_) => unreachable!(), // Safe (type state) + } + } +} From 3654b947996f4a52000e323ed6e642e7015cf30c Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:52:33 +1100 Subject: [PATCH 040/232] Separate impl blocks --- livekit-datatrack/src/local/mod.rs | 2 ++ livekit-datatrack/src/remote/mod.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index eb60f7cea..2e695fcfb 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -36,7 +36,9 @@ impl DataTrack { DataTrackInner::Remote(_) => unreachable!(), // Safe (type state) } } +} +impl DataTrack { /// Publish a frame onto the track. pub fn publish(&self, frame: impl Into) -> Result<(), PublishFrameError> { self.inner().publish(frame.into()) diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index ccaf8d76f..28c1febf5 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -36,3 +36,7 @@ impl DataTrack { } } } + +impl DataTrack { + +} \ No newline at end of file From 6898fb44a3f68efc942d9d1a606f196338885925 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:11:24 +1100 Subject: [PATCH 041/232] Remote skeleton --- livekit-datatrack/src/remote/manager.rs | 115 ++++++++++++++++-------- 1 file changed, 80 insertions(+), 35 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index aaeff606a..4663793ef 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -12,39 +12,84 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::sync::Arc; + +use anyhow::Context; +use tokio::sync::mpsc; +use tokio_stream::Stream; + +use crate::{DataTrack, DecryptionProvider, InternalError, Remote}; + #[derive(Debug, Clone)] -pub(crate) struct TrackInner {} - -// #[derive(Debug)] -// pub struct SubManagerOptions { -// // Dependencies -// // - E2EE -// // - Signaling -// // - Tx UpdateDataSubscription -// // - Rx DataTrackPublishedResponse, DataTrackUnpublishedResponse, -// // - Data track channel -// // - Rx -// } - -// #[derive(Debug)] -// pub struct SubManager { -// options: SubManagerOptions, -// sub_tracks: DashMap -// } - -// impl SubManager { -// pub fn new(options: SubManagerOptions) -> Self { -// Self { options, sub_tracks: DashMap::default() } -// } -// } - -// // handles: track published/unpublished -// // maintains state -// // creates DataTrack when track published -// // send signal to existing ones when track unpublished - -// #[derive(Debug)] -// pub struct Descriptor { -// // tx -// // data channel -> dtp decode -> frame -> tx -// } \ No newline at end of file +pub(crate) struct TrackInner { + // frame_rx + // state... +} + +impl TrackInner { + // manage subscription +} + +impl Drop for TrackInner { + fn drop(&mut self) { + // unsubscribe + } +} + +struct TrackTask { + // depacketizer + // decryption + // state_rx (from manager) + // frame_tx (to track inner) + // packet_in_rx + // signal_out_tx +} + +impl TrackTask { + async fn run(mut self) -> Result<(), InternalError> { + Ok(()) + } +} + +#[derive(Debug)] +pub struct SubManagerOptions { + pub decryption: Option>, +} + +pub struct Manager { + signal_in_tx: mpsc::Sender, + // sub request +} + +impl Manager { + pub fn new( + options: SubManagerOptions, + ) -> ( + Self, + ManagerTask, /*,impl Stream, impl Stream>*/ + ) { + todo!() + } + + /// Handles a signal message from the SFU. + /// + /// In order to function correctly, all message types enumerated in [`SubSignalInput`] + /// must be forwarded here. + /// + pub fn handle_signal(&self, message: SubSignalInput) -> Result<(), InternalError> { + Ok(self.signal_in_tx.try_send(message).context("Failed to handle signal input")?) + } +} + +pub struct ManagerTask { + decryption: Option>, +} + +impl ManagerTask { + pub async fn run(mut self) -> Result<(), InternalError> { + Ok(()) + } +} + +pub enum SubSignalInput {} +pub enum SubSignalOutput {} From 82eb90c02815887714c38bb48cd7d7d7559fa2f3 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:23:52 +1100 Subject: [PATCH 042/232] Define signal I/O --- livekit-datatrack/src/remote/manager.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 4663793ef..369b0c338 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; - +use crate::{DataTrack, DecryptionProvider, InternalError, Remote}; use anyhow::Context; +use from_variants::FromVariants; +use std::sync::Arc; use tokio::sync::mpsc; use tokio_stream::Stream; - -use crate::{DataTrack, DecryptionProvider, InternalError, Remote}; +use livekit_protocol::{self as proto}; #[derive(Debug, Clone)] pub(crate) struct TrackInner { @@ -91,5 +91,16 @@ impl ManagerTask { } } -pub enum SubSignalInput {} -pub enum SubSignalOutput {} +/// Signal message produced by [`SubManager`] to be forwarded to the SFU. +#[derive(Debug, FromVariants)] +pub enum SubSignalOutput { + UpdateSubscription(proto::UpdateDataSubscription) +} + +/// Signal message received from the SFU handled by [`SubManager`]. +#[derive(Debug, FromVariants)] +pub enum SubSignalInput { + ParticipantUpdate(proto::ParticipantUpdate), + UnpublishResponse(proto::UnpublishDataTrackResponse), + SubscriberHandles(proto::DataTrackSubscriberHandles) +} From fc380e2945672543e88818fe2e83acc7a9b35f04 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:24:17 +1100 Subject: [PATCH 043/232] Format --- livekit-datatrack/src/local/manager.rs | 2 +- livekit-datatrack/src/remote/manager.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 1d771a90e..ed0703ac6 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -22,7 +22,7 @@ use anyhow::{anyhow, Context}; use bytes::Bytes; use from_variants::FromVariants; use futures_util::Stream; -use livekit_protocol::{self as proto}; +use livekit_protocol as proto; use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::{ sync::{mpsc, oneshot, watch}, diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 369b0c338..5049ff636 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -18,7 +18,7 @@ use from_variants::FromVariants; use std::sync::Arc; use tokio::sync::mpsc; use tokio_stream::Stream; -use livekit_protocol::{self as proto}; +use livekit_protocol as proto; #[derive(Debug, Clone)] pub(crate) struct TrackInner { From 9c6d13355bf1f59cb39b18fb124cfad2044ed972 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:32:55 +1100 Subject: [PATCH 044/232] Define channels, new for remote manager --- livekit-datatrack/src/remote/manager.rs | 35 ++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 5049ff636..9aa8295f2 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -15,10 +15,10 @@ use crate::{DataTrack, DecryptionProvider, InternalError, Remote}; use anyhow::Context; use from_variants::FromVariants; +use livekit_protocol as proto; use std::sync::Arc; use tokio::sync::mpsc; -use tokio_stream::Stream; -use livekit_protocol as proto; +use tokio_stream::{wrappers::ReceiverStream, Stream}; #[derive(Debug, Clone)] pub(crate) struct TrackInner { @@ -56,19 +56,39 @@ pub struct SubManagerOptions { pub decryption: Option>, } +/// Manager for remote data tracks. pub struct Manager { signal_in_tx: mpsc::Sender, // sub request } impl Manager { + const CH_BUFFER_SIZE: usize = 4; + pub fn new( options: SubManagerOptions, ) -> ( Self, - ManagerTask, /*,impl Stream, impl Stream>*/ + ManagerTask, + impl Stream, + impl Stream>, ) { - todo!() + let (signal_in_tx, signal_in_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); + let (signal_out_tx, signal_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); + let (track_out_tx, track_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); + + let manager = Manager { signal_in_tx }; + let task = ManagerTask { + decryption: options.decryption, + signal_in_rx, + signal_out_tx, + track_out_tx, + }; + + let signal_out_stream = ReceiverStream::new(signal_out_rx); + let track_out_stream = ReceiverStream::new(track_out_rx); + + (manager, task, signal_out_stream, track_out_stream) } /// Handles a signal message from the SFU. @@ -83,6 +103,9 @@ impl Manager { pub struct ManagerTask { decryption: Option>, + signal_in_rx: mpsc::Receiver, + signal_out_tx: mpsc::Sender, + track_out_tx: mpsc::Sender>, } impl ManagerTask { @@ -94,7 +117,7 @@ impl ManagerTask { /// Signal message produced by [`SubManager`] to be forwarded to the SFU. #[derive(Debug, FromVariants)] pub enum SubSignalOutput { - UpdateSubscription(proto::UpdateDataSubscription) + UpdateSubscription(proto::UpdateDataSubscription), } /// Signal message received from the SFU handled by [`SubManager`]. @@ -102,5 +125,5 @@ pub enum SubSignalOutput { pub enum SubSignalInput { ParticipantUpdate(proto::ParticipantUpdate), UnpublishResponse(proto::UnpublishDataTrackResponse), - SubscriberHandles(proto::DataTrackSubscriberHandles) + SubscriberHandles(proto::DataTrackSubscriberHandles), } From 00ece412184e3b5d8803c056e331e6bcd517e971 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:33:04 +1100 Subject: [PATCH 045/232] Docs --- livekit-datatrack/src/local/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index ed0703ac6..ae39b5773 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -137,7 +137,7 @@ pub struct PubManagerOptions { pub encryption: Option>, } -/// Manager for data track publications. +/// Manager for local data tracks. #[derive(Debug, Clone)] pub struct Manager { signal_in_tx: mpsc::Sender, From ab1432b602c489a105230deeb423ccd5d43fc9fd Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:36:53 +1100 Subject: [PATCH 046/232] Allow cloning data track frame --- livekit-datatrack/src/common/frame.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit-datatrack/src/common/frame.rs b/livekit-datatrack/src/common/frame.rs index 00c030deb..e79462469 100644 --- a/livekit-datatrack/src/common/frame.rs +++ b/livekit-datatrack/src/common/frame.rs @@ -18,7 +18,7 @@ use bytes::Bytes; /// /// Construct using [`DataTrackFrameBuilder`]. /// -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct DataTrackFrame { pub(crate) payload: Bytes, pub(crate) user_timestamp: Option, From 5fbbe848ea2aaf71588611da9e55476bb194298b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:54:00 +1100 Subject: [PATCH 047/232] Handle packet input --- livekit-datatrack/src/remote/manager.rs | 48 ++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 9aa8295f2..73d76578e 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -14,6 +14,7 @@ use crate::{DataTrack, DecryptionProvider, InternalError, Remote}; use anyhow::Context; +use bytes::Bytes; use from_variants::FromVariants; use livekit_protocol as proto; use std::sync::Arc; @@ -59,6 +60,7 @@ pub struct SubManagerOptions { /// Manager for remote data tracks. pub struct Manager { signal_in_tx: mpsc::Sender, + packet_in_tx: mpsc::Sender, // sub request } @@ -76,13 +78,15 @@ impl Manager { let (signal_in_tx, signal_in_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); let (signal_out_tx, signal_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); let (track_out_tx, track_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); + let (packet_in_tx, packet_in_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); - let manager = Manager { signal_in_tx }; + let manager = Manager { signal_in_tx, packet_in_tx }; let task = ManagerTask { decryption: options.decryption, signal_in_rx, signal_out_tx, track_out_tx, + packet_in_rx }; let signal_out_stream = ReceiverStream::new(signal_out_rx); @@ -99,6 +103,11 @@ impl Manager { pub fn handle_signal(&self, message: SubSignalInput) -> Result<(), InternalError> { Ok(self.signal_in_tx.try_send(message).context("Failed to handle signal input")?) } + + /// Handles a packet received over the transport. + pub fn handle_packet(&self, packet: Bytes) -> Result<(), InternalError> { + Ok(self.packet_in_tx.try_send(packet).context("Failed to packet")?) + } } pub struct ManagerTask { @@ -106,24 +115,53 @@ pub struct ManagerTask { signal_in_rx: mpsc::Receiver, signal_out_tx: mpsc::Sender, track_out_tx: mpsc::Sender>, + packet_in_rx: mpsc::Receiver } impl ManagerTask { pub async fn run(mut self) -> Result<(), InternalError> { - Ok(()) + loop { + tokio::select! { + biased; // Handle signal input first + // TODO: check cancellation + Some(signal) = self.signal_in_rx.recv() => self.handle_signal(signal), + Some(packet) = self.packet_in_rx.recv() => self.handle_packet(packet), + else => Ok(()) + } + .inspect_err(|err| log::error!("{}", err)) + .ok(); + } + } + + fn handle_packet(&mut self, packet: Bytes) -> Result<(), InternalError> { + todo!() + } + + fn handle_signal(&mut self, message: SubSignalInput) -> Result<(), InternalError> { + match message { + SubSignalInput::ParticipantUpdate(message) => self.handle_participant_update(message), + SubSignalInput::SubscriberHandles(message) => self.handle_subscriber_handles(message) + } + } + + fn handle_participant_update(&mut self, message: proto::ParticipantUpdate) -> Result<(), InternalError> { + todo!() + } + + fn handle_subscriber_handles(&mut self, message: proto::DataTrackSubscriberHandles) -> Result<(), InternalError> { + todo!() } } -/// Signal message produced by [`SubManager`] to be forwarded to the SFU. +/// Signal message produced by [`Manager`] to be forwarded to the SFU. #[derive(Debug, FromVariants)] pub enum SubSignalOutput { UpdateSubscription(proto::UpdateDataSubscription), } -/// Signal message received from the SFU handled by [`SubManager`]. +/// Signal message received from the SFU handled by [`Manager`]. #[derive(Debug, FromVariants)] pub enum SubSignalInput { ParticipantUpdate(proto::ParticipantUpdate), - UnpublishResponse(proto::UnpublishDataTrackResponse), SubscriberHandles(proto::DataTrackSubscriberHandles), } From e15c61c0c893d54c3598e739fa3f3a9fc9ecce9b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:37:12 +1100 Subject: [PATCH 048/232] Handle sync state --- livekit-datatrack/src/local/manager.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index ae39b5773..43193df78 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -255,6 +255,7 @@ impl ManagerTask { PubSignalInput::PublishResponse(res) => self.handle_publish_response(res), PubSignalInput::UnpublishResponse(res) => self.handle_unpublish_response(res), PubSignalInput::RequestResponse(res) => self.handle_request_response(res), + PubSignalInput::SyncState(res) => self.handle_sync_state(res), } } @@ -345,6 +346,17 @@ impl ManagerTask { } Ok(()) } + + fn handle_sync_state( + &mut self, + res: proto::SyncState + ) -> Result<(), InternalError> { + for res in res.publish_data_tracks { + // Forward to standard response handler + self.handle_publish_response(res)? + } + Ok(()) + } } #[derive(Debug, FromVariants)] @@ -366,6 +378,7 @@ pub enum PubSignalInput { PublishResponse(proto::PublishDataTrackResponse), UnpublishResponse(proto::UnpublishDataTrackResponse), RequestResponse(proto::RequestResponse), + SyncState(proto::SyncState) } impl DataTrackOptions { From 54f707567c2c14296afa96df9d99a7fa39f4538c Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:27:57 +1100 Subject: [PATCH 049/232] Protocol abstraction layer --- livekit-datatrack/src/common/mod.rs | 3 +- livekit-datatrack/src/common/proto.rs | 31 +++++ livekit-datatrack/src/common/track.rs | 2 +- livekit-datatrack/src/local/manager.rs | 16 +-- livekit-datatrack/src/remote/manager.rs | 160 ++++++++++++------------ livekit-datatrack/src/remote/mod.rs | 3 +- livekit-datatrack/src/remote/proto.rs | 76 +++++++++++ 7 files changed, 196 insertions(+), 95 deletions(-) create mode 100644 livekit-datatrack/src/common/proto.rs create mode 100644 livekit-datatrack/src/remote/proto.rs diff --git a/livekit-datatrack/src/common/mod.rs b/livekit-datatrack/src/common/mod.rs index 90b3e9189..dff4ea757 100644 --- a/livekit-datatrack/src/common/mod.rs +++ b/livekit-datatrack/src/common/mod.rs @@ -17,8 +17,9 @@ use thiserror::Error; pub mod e2ee; pub mod frame; pub mod track; +pub mod proto; -pub use {e2ee::*, frame::*, track::*}; +pub use {e2ee::*, frame::*, track::*, proto::*}; #[derive(Debug, Error)] #[error(transparent)] diff --git a/livekit-datatrack/src/common/proto.rs b/livekit-datatrack/src/common/proto.rs new file mode 100644 index 000000000..da9bd104f --- /dev/null +++ b/livekit-datatrack/src/common/proto.rs @@ -0,0 +1,31 @@ +// Copyright 2025 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 crate::{dtp::TrackHandle, DataTrackInfo, InternalError}; +use anyhow::anyhow; +use livekit_protocol as proto; + +impl TryInto for proto::DataTrackInfo { + type Error = InternalError; + + fn try_into(self) -> Result { + let handle: TrackHandle = self.pub_handle.try_into().map_err(anyhow::Error::from)?; + let uses_e2ee = match self.encryption() { + proto::encryption::Type::None => false, + proto::encryption::Type::Gcm => true, + other => Err(anyhow!("Unsupported E2EE type: {:?}", other))?, + }; + Ok(DataTrackInfo { handle, sid: self.sid, name: self.name, uses_e2ee }) + } +} diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs index 3309ac018..00d510535 100644 --- a/livekit-datatrack/src/common/track.rs +++ b/livekit-datatrack/src/common/track.rs @@ -16,7 +16,7 @@ use from_variants::FromVariants; use std::{marker::PhantomData, sync::Arc}; use crate::dtp::TrackHandle; -/// Information about a data track. +/// Information about a published data track. #[derive(Debug, Clone)] pub struct DataTrackInfo { pub(crate) sid: String, // TODO: use shared ID type diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 43193df78..ce389c23e 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -414,27 +414,13 @@ impl From for PublishError { } impl TryInto for proto::PublishDataTrackResponse { - type Error = anyhow::Error; + type Error = InternalError; fn try_into(self) -> Result { let info = self.info.context("Missing info")?; info.try_into() } } -impl TryInto for proto::DataTrackInfo { - type Error = anyhow::Error; - - fn try_into(self) -> Result { - let handle: TrackHandle = self.pub_handle.try_into()?; - let uses_e2ee = match self.encryption() { - proto::encryption::Type::None => false, - proto::encryption::Type::Gcm => true, - other => Err(anyhow!("Unsupported E2EE type: {:?}", other))?, - }; - Ok(DataTrackInfo { handle, sid: self.sid, name: self.name, uses_e2ee }) - } -} - impl From for proto::signal_request::Message { fn from(output: PubSignalOutput) -> Self { use proto::signal_request::Message; diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 73d76578e..17db2b03b 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -12,12 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{DataTrack, DecryptionProvider, InternalError, Remote}; +use crate::{ + dtp::TrackHandle, DataTrack, DataTrackInfo, DecryptionProvider, InternalError, Remote, +}; use anyhow::Context; use bytes::Bytes; use from_variants::FromVariants; -use livekit_protocol as proto; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use tokio::sync::mpsc; use tokio_stream::{wrappers::ReceiverStream, Stream}; @@ -53,115 +54,120 @@ impl TrackTask { } #[derive(Debug)] -pub struct SubManagerOptions { +pub struct ManagerOptions { pub decryption: Option>, } /// Manager for remote data tracks. pub struct Manager { - signal_in_tx: mpsc::Sender, - packet_in_tx: mpsc::Sender, - // sub request + event_in_tx: mpsc::Sender, // sub request } impl Manager { const CH_BUFFER_SIZE: usize = 4; - pub fn new( - options: SubManagerOptions, - ) -> ( - Self, - ManagerTask, - impl Stream, - impl Stream>, - ) { - let (signal_in_tx, signal_in_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); - let (signal_out_tx, signal_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); - let (track_out_tx, track_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); - let (packet_in_tx, packet_in_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); - - let manager = Manager { signal_in_tx, packet_in_tx }; - let task = ManagerTask { - decryption: options.decryption, - signal_in_rx, - signal_out_tx, - track_out_tx, - packet_in_rx - }; - - let signal_out_stream = ReceiverStream::new(signal_out_rx); - let track_out_stream = ReceiverStream::new(track_out_rx); - - (manager, task, signal_out_stream, track_out_stream) - } + /// Creates a new manager with the specified options. + pub fn new(options: ManagerOptions) -> (Self, ManagerTask, impl Stream) { + let (event_in_tx, event_in_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); + let (event_out_tx, event_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); + + let manager = Manager { event_in_tx }; + let task = ManagerTask { decryption: options.decryption, event_in_rx, event_out_tx }; - /// Handles a signal message from the SFU. - /// - /// In order to function correctly, all message types enumerated in [`SubSignalInput`] - /// must be forwarded here. - /// - pub fn handle_signal(&self, message: SubSignalInput) -> Result<(), InternalError> { - Ok(self.signal_in_tx.try_send(message).context("Failed to handle signal input")?) + let event_out_stream = ReceiverStream::new(event_out_rx); + (manager, task, event_out_stream) } - /// Handles a packet received over the transport. - pub fn handle_packet(&self, packet: Bytes) -> Result<(), InternalError> { - Ok(self.packet_in_tx.try_send(packet).context("Failed to packet")?) + /// Handles an external event. + pub fn handle_event(&self, event: InputEvent) -> Result<(), InternalError> { + Ok(self.event_in_tx.try_send(event).context("Failed to send input event")?) } } pub struct ManagerTask { decryption: Option>, - signal_in_rx: mpsc::Receiver, - signal_out_tx: mpsc::Sender, - track_out_tx: mpsc::Sender>, - packet_in_rx: mpsc::Receiver + event_in_rx: mpsc::Receiver, + event_out_tx: mpsc::Sender, } impl ManagerTask { - pub async fn run(mut self) -> Result<(), InternalError> { - loop { - tokio::select! { - biased; // Handle signal input first - // TODO: check cancellation - Some(signal) = self.signal_in_rx.recv() => self.handle_signal(signal), - Some(packet) = self.packet_in_rx.recv() => self.handle_packet(packet), - else => Ok(()) - } - .inspect_err(|err| log::error!("{}", err)) - .ok(); + pub async fn run(mut self) { + // TODO: check cancellation + while let Some(event) = self.event_in_rx.recv().await { + let Err(err) = self.handle_event(event) else { continue }; + log::error!("Failed to handle input event: {}", err); } } - fn handle_packet(&mut self, packet: Bytes) -> Result<(), InternalError> { - todo!() + fn handle_event(&mut self, event: InputEvent) -> Result<(), InternalError> { + match event { + InputEvent::PublicationsUpdated(event) => self.handle_publications_updated(event), + InputEvent::SubscriberHandles(event) => self.handle_subscriber_handles(event), + InputEvent::PacketReceived(bytes) => self.handle_packet_received(bytes), + } } - fn handle_signal(&mut self, message: SubSignalInput) -> Result<(), InternalError> { - match message { - SubSignalInput::ParticipantUpdate(message) => self.handle_participant_update(message), - SubSignalInput::SubscriberHandles(message) => self.handle_subscriber_handles(message) - } + fn handle_publications_updated( + &mut self, + event: PublicationsUpdatedEvent, + ) -> Result<(), InternalError> { + todo!() } - fn handle_participant_update(&mut self, message: proto::ParticipantUpdate) -> Result<(), InternalError> { + fn handle_subscriber_handles( + &mut self, + event: SubscriberHandlesEvent, + ) -> Result<(), InternalError> { todo!() } - fn handle_subscriber_handles(&mut self, message: proto::DataTrackSubscriberHandles) -> Result<(), InternalError> { + fn handle_packet_received(&mut self, bytes: Bytes) -> Result<(), InternalError> { todo!() } } -/// Signal message produced by [`Manager`] to be forwarded to the SFU. -#[derive(Debug, FromVariants)] -pub enum SubSignalOutput { - UpdateSubscription(proto::UpdateDataSubscription), +/// An external event handled by [`Manager`]. +#[derive(Debug, Clone, FromVariants)] +pub enum InputEvent { + PublicationsUpdated(PublicationsUpdatedEvent), + SubscriberHandles(SubscriberHandlesEvent), + /// Packet has been received over the transport. + PacketReceived(Bytes), +} + +/// An event produced by [`Manager`] requiring external action. +#[derive(Debug, Clone, FromVariants)] +pub enum OutputEvent { + SubscriptionUpdated(SubscriptionUpdatedEvent), + /// Track has been published and a track object has been created for + /// the user to interact with. + TrackAvailable(DataTrack), } -/// Signal message received from the SFU handled by [`Manager`]. -#[derive(Debug, FromVariants)] -pub enum SubSignalInput { - ParticipantUpdate(proto::ParticipantUpdate), - SubscriberHandles(proto::DataTrackSubscriberHandles), +/// Track publications updated for a specific participant. +/// +/// This is used to detect newly published tracks as well as +/// tracks that have been unpublished. +/// +#[derive(Debug, Clone)] +pub struct PublicationsUpdatedEvent { + /// Mapping between participant identity and data tracks published by that participant. + pub tracks_by_participant: HashMap>, +} + +/// Subscriber handles available or updated. +#[derive(Debug, Clone)] +pub struct SubscriberHandlesEvent { + /// Mapping between track handles attached to incoming packets to the + /// track SIDs they belong to. + pub mapping: HashMap, +} + +/// User subscribed or unsubscribed to a track. +#[derive(Debug, Clone)] +pub struct SubscriptionUpdatedEvent { + /// Identifier of the affected track. + pub track_sid: String, + /// Whether to subscribe or unsubscribe. + pub subscribe: bool, } diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 28c1febf5..5706558f8 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -16,12 +16,13 @@ use crate::{DataTrack, DataTrackInfo, DataTrackInner}; use std::{marker::PhantomData, sync::Arc}; pub(crate) mod manager; +pub(crate) mod proto; /// Data track published by a remote participant. pub type RemoteDataTrack = DataTrack; /// Marker type indicating a [`DataTrack`] belongs to a remote participant. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Remote; impl DataTrack { diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs new file mode 100644 index 000000000..f99a00062 --- /dev/null +++ b/livekit-datatrack/src/remote/proto.rs @@ -0,0 +1,76 @@ +use super::manager::{PublicationsUpdatedEvent, SubscriberHandlesEvent, SubscriptionUpdatedEvent}; +use crate::{dtp::TrackHandle, DataTrackInfo, InternalError}; +use livekit_protocol::{self as proto, ParticipantInfo}; +use std::{collections::HashMap, mem}; + +// MARK: - Protocol -> input event + +impl TryFrom for SubscriberHandlesEvent { + type Error = InternalError; + + fn try_from(msg: proto::DataTrackSubscriberHandles) -> Result { + let mapping = msg + .sub_handles + .into_iter() + .map(|(handle, info)| -> Result<_, InternalError> { + let handle = TrackHandle::try_from(handle).map_err(anyhow::Error::from)?; + Ok((handle, info.track_sid)) + }) + .collect::, _>>()?; + Ok(SubscriberHandlesEvent { mapping }) + } +} + +/// Extracts a [`PublicationsUpdatedEvent`] from a join response. +/// +/// This takes ownership of the `data_tracks` vector for each participant +/// (except for the local participant), leaving an empty vector in its place. +/// +pub fn event_from_join( + msg: &mut proto::JoinResponse, +) -> Result { + event_from_participant_info(&mut msg.other_participants) +} + +/// Extracts a [`PublicationsUpdatedEvent`] from a participant update. +/// +/// This takes ownership of the `data_tracks` vector for each participant in +/// the update, leaving an empty vector in its place. +/// +pub fn event_from_participant_update( + msg: &mut proto::ParticipantUpdate, +) -> Result { + event_from_participant_info(&mut msg.participants) +} + +fn event_from_participant_info( + msg: &mut Vec, +) -> Result { + let tracks_by_participant = msg + .iter_mut() + .map(|participant| -> Result<_, InternalError> { + Ok((participant.identity.clone(), extract_track_info(participant)?)) + }) + .collect::>, _>>()?; + Ok(PublicationsUpdatedEvent { tracks_by_participant }) +} + +fn extract_track_info(msg: &mut ParticipantInfo) -> Result, InternalError> { + mem::take(&mut msg.data_tracks) + .into_iter() + .map(TryInto::::try_into) + .collect::, InternalError>>() +} + +// MARK: - Output event -> protocol + +impl From for proto::UpdateDataSubscription { + fn from(event: SubscriptionUpdatedEvent) -> Self { + let update = proto::update_data_subscription::Update { + track_sid: event.track_sid, + subscribe: event.subscribe, + options: Default::default(), // TODO: pass through options + }; + Self { updates: vec![update] } + } +} From a29cf2fb0e7676463c100d6ca6f832ab7e3b046b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:38:01 +1100 Subject: [PATCH 050/232] Module organization --- livekit-datatrack/src/common/mod.rs | 4 +- livekit-datatrack/src/common/track.rs | 6 +- livekit-datatrack/src/local/manager.rs | 114 ++------------------- livekit-datatrack/src/local/mod.rs | 5 +- livekit-datatrack/src/local/track.rs | 126 ++++++++++++++++++++++++ livekit-datatrack/src/remote/manager.rs | 103 +++++++------------ livekit-datatrack/src/remote/mod.rs | 9 +- livekit-datatrack/src/remote/proto.rs | 14 +++ livekit-datatrack/src/remote/track.rs | 46 +++++++++ 9 files changed, 240 insertions(+), 187 deletions(-) create mode 100644 livekit-datatrack/src/local/track.rs create mode 100644 livekit-datatrack/src/remote/track.rs diff --git a/livekit-datatrack/src/common/mod.rs b/livekit-datatrack/src/common/mod.rs index dff4ea757..f168221b6 100644 --- a/livekit-datatrack/src/common/mod.rs +++ b/livekit-datatrack/src/common/mod.rs @@ -16,10 +16,10 @@ use thiserror::Error; pub mod e2ee; pub mod frame; -pub mod track; pub mod proto; +pub mod track; -pub use {e2ee::*, frame::*, track::*, proto::*}; +pub use {e2ee::*, frame::*, proto::*, track::*}; #[derive(Debug, Error)] #[error(transparent)] diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs index 00d510535..6f00911f5 100644 --- a/livekit-datatrack/src/common/track.rs +++ b/livekit-datatrack/src/common/track.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::dtp::TrackHandle; use from_variants::FromVariants; use std::{marker::PhantomData, sync::Arc}; -use crate::dtp::TrackHandle; /// Information about a published data track. #[derive(Debug, Clone)] @@ -56,8 +56,8 @@ pub struct DataTrack { #[derive(Debug, Clone, FromVariants)] pub(crate) enum DataTrackInner { - Local(crate::local::manager::TrackInner), - Remote(crate::remote::manager::TrackInner), + Local(crate::local::track::TrackInner), + Remote(crate::remote::track::TrackInner), } impl DataTrack { diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index ce389c23e..d1a67bc05 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::Local; +use super::{ + track::{TrackInner, TrackTask}, + Local, +}; use crate::dtp::TrackHandle; use crate::{ dtp, DataTrack, DataTrackFrame, DataTrackInfo, DataTrackOptions, DataTrackState, @@ -30,108 +33,6 @@ use tokio::{ }; use tokio_stream::wrappers::ReceiverStream; -#[derive(Debug, Clone)] -pub(crate) struct TrackInner { - frame_tx: mpsc::Sender, - state_tx: watch::Sender, -} - -impl TrackInner { - pub fn publish(&self, frame: DataTrackFrame) -> Result<(), PublishFrameError> { - if !self.is_published() { - return Err(PublishFrameError::new(frame, PublishFrameErrorReason::TrackUnpublished)); - } - self.frame_tx.try_send(frame).map_err(|err| { - PublishFrameError::new(err.into_inner(), PublishFrameErrorReason::Dropped) - }) - } - - pub fn is_published(&self) -> bool { - matches!(*self.state_tx.borrow(), DataTrackState::Published) - } - - pub fn unpublish(&self) { - self.state_tx - .send(DataTrackState::Unpublished { sfu_initiated: false }) - .inspect_err(|err| log::error!("Failed to update state to unsubscribed: {err}")) - .ok(); - } -} - -impl Drop for TrackInner { - fn drop(&mut self) { - // Implicit unpublish when handle dropped. - self.unpublish(); - } -} - -/// Task responsible for operating an individual published data track. -struct TrackTask { - // TODO: packetizer, e2ee_provider, rate tracking, etc. - packetizer: dtp::Packetizer, - encryption: Option>, - info: Arc, - state_rx: watch::Receiver, - frame_rx: mpsc::Receiver, - packet_out_tx: mpsc::Sender, - signal_out_tx: mpsc::Sender, -} - -impl TrackTask { - async fn run(mut self) -> Result<(), InternalError> { - let mut state = DataTrackState::Published; - while matches!(state, DataTrackState::Published) { - tokio::select! { - _ = self.state_rx.changed() => { - let _: () = state = *self.state_rx.borrow(); - Ok(()) - }, - Some(frame) = self.frame_rx.recv() => self.publish_frame(frame), - else => break - } - .inspect_err(|err| log::error!("{}", err)) - .ok(); - } - if let DataTrackState::Unpublished { sfu_initiated } = state { - if !sfu_initiated { - self.send_unpublish_req()?; - } - } - Ok(()) - } - - fn publish_frame(&mut self, mut frame: DataTrackFrame) -> Result<(), InternalError> { - let mut e2ee: Option = None; - if let Some(encryption) = &self.encryption { - debug_assert!(self.info.uses_e2ee); - let encrypted_payload = - encryption.encrypt(frame.payload).context("Failed to encrypt frame")?; - e2ee = Some(dtp::E2ee { - key_index: encrypted_payload.key_index, - iv: encrypted_payload.iv, - }); - frame.payload = encrypted_payload.payload; - } - - let frame = dtp::PacketizerFrame { - payload: frame.payload, - e2ee, - user_timestamp: frame.user_timestamp, - }; - let packets = self.packetizer.packetize(frame).context("Failed to packetize frame")?; - for packet in packets { - let serialized = packet.serialize(); - self.packet_out_tx.try_send(serialized).context("Failed to send packet")?; - } - Ok(()) - } - - fn send_unpublish_req(self) -> Result<(), InternalError> { - let req = proto::UnpublishDataTrackRequest { pub_handle: self.info.handle.into() }; - Ok(self.signal_out_tx.try_send(req.into()).context("Failed to send unpublish")?) - } -} - #[derive(Debug)] pub struct PubManagerOptions { pub encryption: Option>, @@ -347,10 +248,7 @@ impl ManagerTask { Ok(()) } - fn handle_sync_state( - &mut self, - res: proto::SyncState - ) -> Result<(), InternalError> { + fn handle_sync_state(&mut self, res: proto::SyncState) -> Result<(), InternalError> { for res in res.publish_data_tracks { // Forward to standard response handler self.handle_publish_response(res)? @@ -378,7 +276,7 @@ pub enum PubSignalInput { PublishResponse(proto::PublishDataTrackResponse), UnpublishResponse(proto::UnpublishDataTrackResponse), RequestResponse(proto::RequestResponse), - SyncState(proto::SyncState) + SyncState(proto::SyncState), } impl DataTrackOptions { diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 2e695fcfb..e11d90ead 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -17,6 +17,7 @@ use std::{fmt, marker::PhantomData, sync::Arc}; use thiserror::Error; pub(crate) mod manager; +pub(crate) mod track; /// Data track published by the local participant. pub type LocalDataTrack = DataTrack; @@ -26,11 +27,11 @@ pub type LocalDataTrack = DataTrack; pub struct Local; impl DataTrack { - pub(crate) fn new(info: Arc, inner: manager::TrackInner) -> Self { + pub(crate) fn new(info: Arc, inner: track::TrackInner) -> Self { Self { info, inner: inner.into(), _location: PhantomData } } - fn inner(&self) -> &manager::TrackInner { + fn inner(&self) -> &track::TrackInner { match &self.inner { DataTrackInner::Local(track) => track, DataTrackInner::Remote(_) => unreachable!(), // Safe (type state) diff --git a/livekit-datatrack/src/local/track.rs b/livekit-datatrack/src/local/track.rs new file mode 100644 index 000000000..a8cc72f89 --- /dev/null +++ b/livekit-datatrack/src/local/track.rs @@ -0,0 +1,126 @@ +// Copyright 2025 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 super::manager::PubSignalOutput; +use crate::{ + dtp, DataTrackFrame, DataTrackInfo, DataTrackState, EncryptionProvider, InternalError, + PublishFrameError, PublishFrameErrorReason, +}; +use anyhow::Context; +use bytes::Bytes; +use livekit_protocol as proto; +use std::sync::Arc; +use tokio::sync::{mpsc, watch}; + +#[derive(Debug, Clone)] +pub(crate) struct TrackInner { + pub frame_tx: mpsc::Sender, + pub state_tx: watch::Sender, +} + +impl TrackInner { + pub fn publish(&self, frame: DataTrackFrame) -> Result<(), PublishFrameError> { + if !self.is_published() { + return Err(PublishFrameError::new(frame, PublishFrameErrorReason::TrackUnpublished)); + } + self.frame_tx.try_send(frame).map_err(|err| { + PublishFrameError::new(err.into_inner(), PublishFrameErrorReason::Dropped) + }) + } + + pub fn is_published(&self) -> bool { + matches!(*self.state_tx.borrow(), DataTrackState::Published) + } + + pub fn unpublish(&self) { + self.state_tx + .send(DataTrackState::Unpublished { sfu_initiated: false }) + .inspect_err(|err| log::error!("Failed to update state to unsubscribed: {err}")) + .ok(); + } +} + +impl Drop for TrackInner { + fn drop(&mut self) { + // Implicit unpublish when handle dropped. + self.unpublish(); + } +} + +/// Task responsible for operating an individual published data track. +pub(super) struct TrackTask { + // TODO: packetizer, e2ee_provider, rate tracking, etc. + pub packetizer: dtp::Packetizer, + pub encryption: Option>, + pub info: Arc, + pub state_rx: watch::Receiver, + pub frame_rx: mpsc::Receiver, + pub packet_out_tx: mpsc::Sender, + pub signal_out_tx: mpsc::Sender, +} + +impl TrackTask { + pub async fn run(mut self) -> Result<(), InternalError> { + let mut state = DataTrackState::Published; + while matches!(state, DataTrackState::Published) { + tokio::select! { + _ = self.state_rx.changed() => { + let _: () = state = *self.state_rx.borrow(); + Ok(()) + }, + Some(frame) = self.frame_rx.recv() => self.publish_frame(frame), + else => break + } + .inspect_err(|err| log::error!("{}", err)) + .ok(); + } + if let DataTrackState::Unpublished { sfu_initiated } = state { + if !sfu_initiated { + self.send_unpublish_req()?; + } + } + Ok(()) + } + + fn publish_frame(&mut self, mut frame: DataTrackFrame) -> Result<(), InternalError> { + let mut e2ee: Option = None; + if let Some(encryption) = &self.encryption { + debug_assert!(self.info.uses_e2ee); + let encrypted_payload = + encryption.encrypt(frame.payload).context("Failed to encrypt frame")?; + e2ee = Some(dtp::E2ee { + key_index: encrypted_payload.key_index, + iv: encrypted_payload.iv, + }); + frame.payload = encrypted_payload.payload; + } + + let frame = dtp::PacketizerFrame { + payload: frame.payload, + e2ee, + user_timestamp: frame.user_timestamp, + }; + let packets = self.packetizer.packetize(frame).context("Failed to packetize frame")?; + for packet in packets { + let serialized = packet.serialize(); + self.packet_out_tx.try_send(serialized).context("Failed to send packet")?; + } + Ok(()) + } + + fn send_unpublish_req(self) -> Result<(), InternalError> { + let req = proto::UnpublishDataTrackRequest { pub_handle: self.info.handle.into() }; + Ok(self.signal_out_tx.try_send(req.into()).context("Failed to send unpublish")?) + } +} diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 17db2b03b..2052c3c76 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -22,35 +22,50 @@ use std::{collections::HashMap, sync::Arc}; use tokio::sync::mpsc; use tokio_stream::{wrappers::ReceiverStream, Stream}; -#[derive(Debug, Clone)] -pub(crate) struct TrackInner { - // frame_rx - // state... +/// An external event handled by [`Manager`]. +#[derive(Debug, Clone, FromVariants)] +pub enum InputEvent { + PublicationsUpdated(PublicationsUpdatedEvent), + SubscriberHandles(SubscriberHandlesEvent), + /// Packet has been received over the transport. + PacketReceived(Bytes), } -impl TrackInner { - // manage subscription +/// An event produced by [`Manager`] requiring external action. +#[derive(Debug, Clone, FromVariants)] +pub enum OutputEvent { + SubscriptionUpdated(SubscriptionUpdatedEvent), + /// Track has been published and a track object has been created for + /// the user to interact with. + TrackAvailable(DataTrack), } -impl Drop for TrackInner { - fn drop(&mut self) { - // unsubscribe - } +/// Track publications updated for a specific participant. +/// +/// This is used to detect newly published tracks as well as +/// tracks that have been unpublished. +/// +#[derive(Debug, Clone)] +pub struct PublicationsUpdatedEvent { + /// Mapping between participant identity and data tracks published by that participant. + pub tracks_by_participant: HashMap>, } -struct TrackTask { - // depacketizer - // decryption - // state_rx (from manager) - // frame_tx (to track inner) - // packet_in_rx - // signal_out_tx +/// Subscriber handles available or updated. +#[derive(Debug, Clone)] +pub struct SubscriberHandlesEvent { + /// Mapping between track handles attached to incoming packets to the + /// track SIDs they belong to. + pub mapping: HashMap, } -impl TrackTask { - async fn run(mut self) -> Result<(), InternalError> { - Ok(()) - } +/// User subscribed or unsubscribed to a track. +#[derive(Debug, Clone)] +pub struct SubscriptionUpdatedEvent { + /// Identifier of the affected track. + pub track_sid: String, + /// Whether to subscribe or unsubscribe. + pub subscribe: bool, } #[derive(Debug)] @@ -125,49 +140,3 @@ impl ManagerTask { todo!() } } - -/// An external event handled by [`Manager`]. -#[derive(Debug, Clone, FromVariants)] -pub enum InputEvent { - PublicationsUpdated(PublicationsUpdatedEvent), - SubscriberHandles(SubscriberHandlesEvent), - /// Packet has been received over the transport. - PacketReceived(Bytes), -} - -/// An event produced by [`Manager`] requiring external action. -#[derive(Debug, Clone, FromVariants)] -pub enum OutputEvent { - SubscriptionUpdated(SubscriptionUpdatedEvent), - /// Track has been published and a track object has been created for - /// the user to interact with. - TrackAvailable(DataTrack), -} - -/// Track publications updated for a specific participant. -/// -/// This is used to detect newly published tracks as well as -/// tracks that have been unpublished. -/// -#[derive(Debug, Clone)] -pub struct PublicationsUpdatedEvent { - /// Mapping between participant identity and data tracks published by that participant. - pub tracks_by_participant: HashMap>, -} - -/// Subscriber handles available or updated. -#[derive(Debug, Clone)] -pub struct SubscriberHandlesEvent { - /// Mapping between track handles attached to incoming packets to the - /// track SIDs they belong to. - pub mapping: HashMap, -} - -/// User subscribed or unsubscribed to a track. -#[derive(Debug, Clone)] -pub struct SubscriptionUpdatedEvent { - /// Identifier of the affected track. - pub track_sid: String, - /// Whether to subscribe or unsubscribe. - pub subscribe: bool, -} diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 5706558f8..a1a4bf32c 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -17,6 +17,7 @@ use std::{marker::PhantomData, sync::Arc}; pub(crate) mod manager; pub(crate) mod proto; +pub(crate) mod track; /// Data track published by a remote participant. pub type RemoteDataTrack = DataTrack; @@ -26,11 +27,11 @@ pub type RemoteDataTrack = DataTrack; pub struct Remote; impl DataTrack { - pub(crate) fn new(info: Arc, inner: manager::TrackInner) -> Self { + pub(crate) fn new(info: Arc, inner: track::TrackInner) -> Self { Self { info, inner: inner.into(), _location: PhantomData } } - fn inner(&self) -> &manager::TrackInner { + fn inner(&self) -> &track::TrackInner { match &self.inner { DataTrackInner::Remote(inner) => inner, DataTrackInner::Local(_) => unreachable!(), // Safe (type state) @@ -38,6 +39,4 @@ impl DataTrack { } } -impl DataTrack { - -} \ No newline at end of file +impl DataTrack {} diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index f99a00062..bb9a2dca2 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -1,3 +1,17 @@ +// Copyright 2025 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 super::manager::{PublicationsUpdatedEvent, SubscriberHandlesEvent, SubscriptionUpdatedEvent}; use crate::{dtp::TrackHandle, DataTrackInfo, InternalError}; use livekit_protocol::{self as proto, ParticipantInfo}; diff --git a/livekit-datatrack/src/remote/track.rs b/livekit-datatrack/src/remote/track.rs new file mode 100644 index 000000000..68fa9ce21 --- /dev/null +++ b/livekit-datatrack/src/remote/track.rs @@ -0,0 +1,46 @@ +// Copyright 2025 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 crate::InternalError; + +#[derive(Debug, Clone)] +pub(crate) struct TrackInner { + // frame_rx + // state... +} + +impl TrackInner { + // manage subscription +} + +impl Drop for TrackInner { + fn drop(&mut self) { + // unsubscribe + } +} + +struct TrackTask { + // depacketizer + // decryption + // state_rx (from manager) + // frame_tx (to track inner) + // packet_in_rx + // signal_out_tx +} + +impl TrackTask { + async fn run(mut self) -> Result<(), InternalError> { + Ok(()) + } +} From 669a7f3b5b9bcadc4f7ce670cfbd597d31b79191 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:40:31 +1100 Subject: [PATCH 051/232] Rename types --- livekit-datatrack/src/common/track.rs | 4 ++-- livekit-datatrack/src/local/manager.rs | 6 +++--- livekit-datatrack/src/local/mod.rs | 4 ++-- livekit-datatrack/src/local/track.rs | 10 +++++----- livekit-datatrack/src/remote/mod.rs | 4 ++-- livekit-datatrack/src/remote/track.rs | 10 +++++----- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs index 6f00911f5..cd1fa24ac 100644 --- a/livekit-datatrack/src/common/track.rs +++ b/livekit-datatrack/src/common/track.rs @@ -56,8 +56,8 @@ pub struct DataTrack { #[derive(Debug, Clone, FromVariants)] pub(crate) enum DataTrackInner { - Local(crate::local::track::TrackInner), - Remote(crate::remote::track::TrackInner), + Local(crate::local::track::LocalTrackInner), + Remote(crate::remote::track::RemoteTrackInner), } impl DataTrack { diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index d1a67bc05..5e6d43a59 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -13,7 +13,7 @@ // limitations under the License. use super::{ - track::{TrackInner, TrackTask}, + track::{LocalTrackInner, LocalTrackTask}, Local, }; use crate::dtp::TrackHandle; @@ -178,7 +178,7 @@ impl ManagerTask { let (state_tx, state_rx) = watch::channel(DataTrackState::Published); let info = Arc::new(info); - let task = TrackTask { + let task = LocalTrackTask { // TODO: handle cancellation packetizer: dtp::Packetizer::new(info.handle, 16_000), encryption: self.encryption.clone(), @@ -191,7 +191,7 @@ impl ManagerTask { livekit_runtime::spawn(task.run()); self.active_publications.insert(info.handle, state_tx.clone()); - let handle = TrackInner { frame_tx, state_tx }; + let handle = LocalTrackInner { frame_tx, state_tx }; DataTrack::::new(info, handle) } diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index e11d90ead..e08bf1e62 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -27,11 +27,11 @@ pub type LocalDataTrack = DataTrack; pub struct Local; impl DataTrack { - pub(crate) fn new(info: Arc, inner: track::TrackInner) -> Self { + pub(crate) fn new(info: Arc, inner: track::LocalTrackInner) -> Self { Self { info, inner: inner.into(), _location: PhantomData } } - fn inner(&self) -> &track::TrackInner { + fn inner(&self) -> &track::LocalTrackInner { match &self.inner { DataTrackInner::Local(track) => track, DataTrackInner::Remote(_) => unreachable!(), // Safe (type state) diff --git a/livekit-datatrack/src/local/track.rs b/livekit-datatrack/src/local/track.rs index a8cc72f89..e04527aee 100644 --- a/livekit-datatrack/src/local/track.rs +++ b/livekit-datatrack/src/local/track.rs @@ -24,12 +24,12 @@ use std::sync::Arc; use tokio::sync::{mpsc, watch}; #[derive(Debug, Clone)] -pub(crate) struct TrackInner { +pub(crate) struct LocalTrackInner { pub frame_tx: mpsc::Sender, pub state_tx: watch::Sender, } -impl TrackInner { +impl LocalTrackInner { pub fn publish(&self, frame: DataTrackFrame) -> Result<(), PublishFrameError> { if !self.is_published() { return Err(PublishFrameError::new(frame, PublishFrameErrorReason::TrackUnpublished)); @@ -51,7 +51,7 @@ impl TrackInner { } } -impl Drop for TrackInner { +impl Drop for LocalTrackInner { fn drop(&mut self) { // Implicit unpublish when handle dropped. self.unpublish(); @@ -59,7 +59,7 @@ impl Drop for TrackInner { } /// Task responsible for operating an individual published data track. -pub(super) struct TrackTask { +pub(super) struct LocalTrackTask { // TODO: packetizer, e2ee_provider, rate tracking, etc. pub packetizer: dtp::Packetizer, pub encryption: Option>, @@ -70,7 +70,7 @@ pub(super) struct TrackTask { pub signal_out_tx: mpsc::Sender, } -impl TrackTask { +impl LocalTrackTask { pub async fn run(mut self) -> Result<(), InternalError> { let mut state = DataTrackState::Published; while matches!(state, DataTrackState::Published) { diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index a1a4bf32c..368c85cd5 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -27,11 +27,11 @@ pub type RemoteDataTrack = DataTrack; pub struct Remote; impl DataTrack { - pub(crate) fn new(info: Arc, inner: track::TrackInner) -> Self { + pub(crate) fn new(info: Arc, inner: track::RemoteTrackInner) -> Self { Self { info, inner: inner.into(), _location: PhantomData } } - fn inner(&self) -> &track::TrackInner { + fn inner(&self) -> &track::RemoteTrackInner { match &self.inner { DataTrackInner::Remote(inner) => inner, DataTrackInner::Local(_) => unreachable!(), // Safe (type state) diff --git a/livekit-datatrack/src/remote/track.rs b/livekit-datatrack/src/remote/track.rs index 68fa9ce21..2e514ef9c 100644 --- a/livekit-datatrack/src/remote/track.rs +++ b/livekit-datatrack/src/remote/track.rs @@ -15,22 +15,22 @@ use crate::InternalError; #[derive(Debug, Clone)] -pub(crate) struct TrackInner { +pub(crate) struct RemoteTrackInner { // frame_rx // state... } -impl TrackInner { +impl RemoteTrackInner { // manage subscription } -impl Drop for TrackInner { +impl Drop for RemoteTrackInner { fn drop(&mut self) { // unsubscribe } } -struct TrackTask { +struct RemoteTrackTask { // depacketizer // decryption // state_rx (from manager) @@ -39,7 +39,7 @@ struct TrackTask { // signal_out_tx } -impl TrackTask { +impl RemoteTrackTask { async fn run(mut self) -> Result<(), InternalError> { Ok(()) } From 27288fb4faf719931278e792dd2ac4aec87f74f8 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:41:42 +1100 Subject: [PATCH 052/232] Remove unused imports --- livekit-datatrack/src/local/manager.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 5e6d43a59..c52db9e0f 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -18,8 +18,8 @@ use super::{ }; use crate::dtp::TrackHandle; use crate::{ - dtp, DataTrack, DataTrackFrame, DataTrackInfo, DataTrackOptions, DataTrackState, - EncryptionProvider, InternalError, PublishError, PublishFrameError, PublishFrameErrorReason, + dtp, DataTrack, DataTrackInfo, DataTrackOptions, DataTrackState, EncryptionProvider, + InternalError, PublishError, }; use anyhow::{anyhow, Context}; use bytes::Bytes; From c4668719c3c759b17923e075b2047c74fb657e6a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:46:43 +1100 Subject: [PATCH 053/232] Module organization --- livekit-datatrack/src/common/track.rs | 4 ++-- livekit-datatrack/src/local/mod.rs | 10 ++++++---- livekit-datatrack/src/local/track.rs | 4 ++-- livekit-datatrack/src/remote/mod.rs | 12 +++++++----- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs index cd1fa24ac..8ed2d5560 100644 --- a/livekit-datatrack/src/common/track.rs +++ b/livekit-datatrack/src/common/track.rs @@ -56,8 +56,8 @@ pub struct DataTrack { #[derive(Debug, Clone, FromVariants)] pub(crate) enum DataTrackInner { - Local(crate::local::track::LocalTrackInner), - Remote(crate::remote::track::RemoteTrackInner), + Local(crate::local::LocalTrackInner), + Remote(crate::remote::RemoteTrackInner), } impl DataTrack { diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index e08bf1e62..7dd8fd37a 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -16,8 +16,10 @@ use crate::{DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, InternalEr use std::{fmt, marker::PhantomData, sync::Arc}; use thiserror::Error; -pub(crate) mod manager; -pub(crate) mod track; +mod manager; +mod track; + +pub(crate) use track::LocalTrackInner; /// Data track published by the local participant. pub type LocalDataTrack = DataTrack; @@ -27,11 +29,11 @@ pub type LocalDataTrack = DataTrack; pub struct Local; impl DataTrack { - pub(crate) fn new(info: Arc, inner: track::LocalTrackInner) -> Self { + pub(crate) fn new(info: Arc, inner: LocalTrackInner) -> Self { Self { info, inner: inner.into(), _location: PhantomData } } - fn inner(&self) -> &track::LocalTrackInner { + fn inner(&self) -> &LocalTrackInner { match &self.inner { DataTrackInner::Local(track) => track, DataTrackInner::Remote(_) => unreachable!(), // Safe (type state) diff --git a/livekit-datatrack/src/local/track.rs b/livekit-datatrack/src/local/track.rs index e04527aee..0cbbe9c68 100644 --- a/livekit-datatrack/src/local/track.rs +++ b/livekit-datatrack/src/local/track.rs @@ -25,8 +25,8 @@ use tokio::sync::{mpsc, watch}; #[derive(Debug, Clone)] pub(crate) struct LocalTrackInner { - pub frame_tx: mpsc::Sender, - pub state_tx: watch::Sender, + pub(super) frame_tx: mpsc::Sender, + pub(super) state_tx: watch::Sender, } impl LocalTrackInner { diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 368c85cd5..ba122c95a 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -15,9 +15,11 @@ use crate::{DataTrack, DataTrackInfo, DataTrackInner}; use std::{marker::PhantomData, sync::Arc}; -pub(crate) mod manager; -pub(crate) mod proto; -pub(crate) mod track; +mod manager; +mod proto; +mod track; + +pub(crate) use track::RemoteTrackInner; /// Data track published by a remote participant. pub type RemoteDataTrack = DataTrack; @@ -27,11 +29,11 @@ pub type RemoteDataTrack = DataTrack; pub struct Remote; impl DataTrack { - pub(crate) fn new(info: Arc, inner: track::RemoteTrackInner) -> Self { + pub(crate) fn new(info: Arc, inner: RemoteTrackInner) -> Self { Self { info, inner: inner.into(), _location: PhantomData } } - fn inner(&self) -> &track::RemoteTrackInner { + fn inner(&self) -> &RemoteTrackInner { match &self.inner { DataTrackInner::Remote(inner) => inner, DataTrackInner::Local(_) => unreachable!(), // Safe (type state) From fc13086f640fbea8da621222f829f2c08807c558 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:00:50 +1100 Subject: [PATCH 054/232] Move track inner into parent module --- livekit-datatrack/src/local/manager.rs | 5 +++- livekit-datatrack/src/local/mod.rs | 41 ++++++++++++++++++++++---- livekit-datatrack/src/local/track.rs | 37 ----------------------- 3 files changed, 39 insertions(+), 44 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index c52db9e0f..6436f9809 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -13,7 +13,8 @@ // limitations under the License. use super::{ - track::{LocalTrackInner, LocalTrackTask}, + LocalTrackInner, + track::{LocalTrackTask}, Local, }; use crate::dtp::TrackHandle; @@ -33,6 +34,8 @@ use tokio::{ }; use tokio_stream::wrappers::ReceiverStream; + + #[derive(Debug)] pub struct PubManagerOptions { pub encryption: Option>, diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 7dd8fd37a..f880dfda1 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -12,15 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, InternalError}; +use crate::{ + DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, DataTrackState, InternalError, +}; use std::{fmt, marker::PhantomData, sync::Arc}; use thiserror::Error; +use tokio::sync::{mpsc, watch}; mod manager; mod track; -pub(crate) use track::LocalTrackInner; - /// Data track published by the local participant. pub type LocalDataTrack = DataTrack; @@ -44,17 +45,45 @@ impl DataTrack { impl DataTrack { /// Publish a frame onto the track. pub fn publish(&self, frame: impl Into) -> Result<(), PublishFrameError> { - self.inner().publish(frame.into()) + let frame = frame.into(); + if !self.is_published() { + return Err(PublishFrameError::new(frame, PublishFrameErrorReason::TrackUnpublished)); + } + self.inner().frame_tx.try_send(frame).map_err(|err| { + PublishFrameError::new(err.into_inner(), PublishFrameErrorReason::Dropped) + }) } /// Whether or not the track is still published. pub fn is_published(&self) -> bool { - self.inner().is_published() + matches!(*self.inner().state_tx.borrow(), DataTrackState::Published) } /// Unpublish the track. pub fn unpublish(self) { - self.inner().unpublish() + self.inner().local_unpublish(); + } +} + +#[derive(Debug, Clone)] +pub(crate) struct LocalTrackInner { + pub frame_tx: mpsc::Sender, + pub state_tx: watch::Sender, +} + +impl LocalTrackInner { + fn local_unpublish(&self) { + self.state_tx + .send(DataTrackState::Unpublished { sfu_initiated: false }) + .inspect_err(|err| log::error!("Failed to update state to unsubscribed: {err}")) + .ok(); + } +} + +impl Drop for LocalTrackInner { + fn drop(&mut self) { + // Implicit unpublish when handle dropped. + self.local_unpublish(); } } diff --git a/livekit-datatrack/src/local/track.rs b/livekit-datatrack/src/local/track.rs index 0cbbe9c68..436a3e432 100644 --- a/livekit-datatrack/src/local/track.rs +++ b/livekit-datatrack/src/local/track.rs @@ -15,7 +15,6 @@ use super::manager::PubSignalOutput; use crate::{ dtp, DataTrackFrame, DataTrackInfo, DataTrackState, EncryptionProvider, InternalError, - PublishFrameError, PublishFrameErrorReason, }; use anyhow::Context; use bytes::Bytes; @@ -23,44 +22,8 @@ use livekit_protocol as proto; use std::sync::Arc; use tokio::sync::{mpsc, watch}; -#[derive(Debug, Clone)] -pub(crate) struct LocalTrackInner { - pub(super) frame_tx: mpsc::Sender, - pub(super) state_tx: watch::Sender, -} - -impl LocalTrackInner { - pub fn publish(&self, frame: DataTrackFrame) -> Result<(), PublishFrameError> { - if !self.is_published() { - return Err(PublishFrameError::new(frame, PublishFrameErrorReason::TrackUnpublished)); - } - self.frame_tx.try_send(frame).map_err(|err| { - PublishFrameError::new(err.into_inner(), PublishFrameErrorReason::Dropped) - }) - } - - pub fn is_published(&self) -> bool { - matches!(*self.state_tx.borrow(), DataTrackState::Published) - } - - pub fn unpublish(&self) { - self.state_tx - .send(DataTrackState::Unpublished { sfu_initiated: false }) - .inspect_err(|err| log::error!("Failed to update state to unsubscribed: {err}")) - .ok(); - } -} - -impl Drop for LocalTrackInner { - fn drop(&mut self) { - // Implicit unpublish when handle dropped. - self.unpublish(); - } -} - /// Task responsible for operating an individual published data track. pub(super) struct LocalTrackTask { - // TODO: packetizer, e2ee_provider, rate tracking, etc. pub packetizer: dtp::Packetizer, pub encryption: Option>, pub info: Arc, From 180854a841a56effd6e6a4f7f385aa43013f829a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:08:43 +1100 Subject: [PATCH 055/232] Wrap inner track in arc --- livekit-datatrack/src/common/track.rs | 2 +- livekit-datatrack/src/local/mod.rs | 4 ++-- livekit-datatrack/src/remote/mod.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs index 8ed2d5560..ad55c03e2 100644 --- a/livekit-datatrack/src/common/track.rs +++ b/livekit-datatrack/src/common/track.rs @@ -49,7 +49,7 @@ impl DataTrackInfo { #[derive(Debug, Clone)] pub struct DataTrack { pub(crate) info: Arc, - pub(crate) inner: DataTrackInner, + pub(crate) inner: Arc, /// Marker indicating local or remote. pub(crate) _location: PhantomData, } diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index f880dfda1..7c544b06e 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -31,11 +31,11 @@ pub struct Local; impl DataTrack { pub(crate) fn new(info: Arc, inner: LocalTrackInner) -> Self { - Self { info, inner: inner.into(), _location: PhantomData } + Self { info, inner: Arc::new(inner.into()), _location: PhantomData } } fn inner(&self) -> &LocalTrackInner { - match &self.inner { + match &*self.inner { DataTrackInner::Local(track) => track, DataTrackInner::Remote(_) => unreachable!(), // Safe (type state) } diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index ba122c95a..d14263f48 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -30,11 +30,11 @@ pub struct Remote; impl DataTrack { pub(crate) fn new(info: Arc, inner: RemoteTrackInner) -> Self { - Self { info, inner: inner.into(), _location: PhantomData } + Self { info, inner: Arc::new(inner.into()), _location: PhantomData } } fn inner(&self) -> &RemoteTrackInner { - match &self.inner { + match &*self.inner { DataTrackInner::Remote(inner) => inner, DataTrackInner::Local(_) => unreachable!(), // Safe (type state) } From 2a5cdf25a2cd25ec94f05117862d1cd8150f0afe Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:13:03 +1100 Subject: [PATCH 056/232] Module visibility --- livekit-datatrack/src/common/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/livekit-datatrack/src/common/mod.rs b/livekit-datatrack/src/common/mod.rs index f168221b6..58ee9df96 100644 --- a/livekit-datatrack/src/common/mod.rs +++ b/livekit-datatrack/src/common/mod.rs @@ -14,12 +14,12 @@ use thiserror::Error; -pub mod e2ee; -pub mod frame; -pub mod proto; -pub mod track; +mod e2ee; +mod frame; +mod proto; +mod track; -pub use {e2ee::*, frame::*, proto::*, track::*}; +pub use {e2ee::*, frame::*, track::*}; #[derive(Debug, Error)] #[error(transparent)] From 1fc76350e042b51cdf41fd664cd53fd7b938a055 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:15:07 +1100 Subject: [PATCH 057/232] Rename module to pipeline --- livekit-datatrack/src/local/manager.rs | 2 +- livekit-datatrack/src/local/mod.rs | 2 +- livekit-datatrack/src/local/{track.rs => pipeline.rs} | 0 livekit-datatrack/src/remote/mod.rs | 4 ++-- livekit-datatrack/src/remote/{track.rs => pipeline.rs} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename livekit-datatrack/src/local/{track.rs => pipeline.rs} (100%) rename livekit-datatrack/src/remote/{track.rs => pipeline.rs} (100%) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 6436f9809..2cea4caaf 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -14,7 +14,7 @@ use super::{ LocalTrackInner, - track::{LocalTrackTask}, + pipeline::{LocalTrackTask}, Local, }; use crate::dtp::TrackHandle; diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 7c544b06e..49fef4e62 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -20,7 +20,7 @@ use thiserror::Error; use tokio::sync::{mpsc, watch}; mod manager; -mod track; +mod pipeline; /// Data track published by the local participant. pub type LocalDataTrack = DataTrack; diff --git a/livekit-datatrack/src/local/track.rs b/livekit-datatrack/src/local/pipeline.rs similarity index 100% rename from livekit-datatrack/src/local/track.rs rename to livekit-datatrack/src/local/pipeline.rs diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index d14263f48..ab3774320 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -17,9 +17,9 @@ use std::{marker::PhantomData, sync::Arc}; mod manager; mod proto; -mod track; +mod pipeline; -pub(crate) use track::RemoteTrackInner; +pub(crate) use pipeline::RemoteTrackInner; /// Data track published by a remote participant. pub type RemoteDataTrack = DataTrack; diff --git a/livekit-datatrack/src/remote/track.rs b/livekit-datatrack/src/remote/pipeline.rs similarity index 100% rename from livekit-datatrack/src/remote/track.rs rename to livekit-datatrack/src/remote/pipeline.rs From a2b021d3f05fb648be3a1a8271cb45eedfe1ab0c Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:16:15 +1100 Subject: [PATCH 058/232] Relocate task inner --- livekit-datatrack/src/remote/mod.rs | 18 ++++++++++++++++-- livekit-datatrack/src/remote/pipeline.rs | 16 ---------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index ab3774320..0d3e24fe4 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -19,8 +19,6 @@ mod manager; mod proto; mod pipeline; -pub(crate) use pipeline::RemoteTrackInner; - /// Data track published by a remote participant. pub type RemoteDataTrack = DataTrack; @@ -42,3 +40,19 @@ impl DataTrack { } impl DataTrack {} + +#[derive(Debug, Clone)] +pub(crate) struct RemoteTrackInner { + // frame_rx + // state... +} + +impl RemoteTrackInner { + // manage subscription +} + +impl Drop for RemoteTrackInner { + fn drop(&mut self) { + // unsubscribe + } +} \ No newline at end of file diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 2e514ef9c..ed9656876 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -14,22 +14,6 @@ use crate::InternalError; -#[derive(Debug, Clone)] -pub(crate) struct RemoteTrackInner { - // frame_rx - // state... -} - -impl RemoteTrackInner { - // manage subscription -} - -impl Drop for RemoteTrackInner { - fn drop(&mut self) { - // unsubscribe - } -} - struct RemoteTrackTask { // depacketizer // decryption From 9c57a92706d9b47484c48391102832d1f0e05254 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:28:07 +1100 Subject: [PATCH 059/232] Use type alias --- livekit-datatrack/src/local/manager.rs | 19 +++++++++---------- livekit-datatrack/src/remote/manager.rs | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 2cea4caaf..291c59964 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -14,10 +14,9 @@ use super::{ LocalTrackInner, - pipeline::{LocalTrackTask}, - Local, + pipeline::{LocalTrackTask} }; -use crate::dtp::TrackHandle; +use crate::{LocalDataTrack, dtp::TrackHandle}; use crate::{ dtp, DataTrack, DataTrackInfo, DataTrackOptions, DataTrackState, EncryptionProvider, InternalError, PublishError, @@ -37,7 +36,7 @@ use tokio_stream::wrappers::ReceiverStream; #[derive(Debug)] -pub struct PubManagerOptions { +pub struct ManagerOptions { pub encryption: Option>, } @@ -53,7 +52,7 @@ impl Manager { const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); pub fn new( - options: PubManagerOptions, + options: ManagerOptions, ) -> (Self, ManagerTask, impl Stream, impl Stream) { let (pub_req_tx, pub_req_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); let (signal_in_tx, signal_in_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); @@ -91,7 +90,7 @@ impl Manager { pub async fn publish_track( &self, options: DataTrackOptions, - ) -> Result, PublishError> { + ) -> Result { let (result_tx, result_rx) = oneshot::channel(); let request = PubRequest { options, result_tx }; self.pub_req_tx.try_send(request).map_err(|_| PublishError::Disconnected)?; @@ -107,7 +106,7 @@ impl Manager { struct PubRequest { options: DataTrackOptions, - result_tx: oneshot::Sender, PublishError>>, + result_tx: oneshot::Sender>, } pub struct ManagerTask { @@ -118,7 +117,7 @@ pub struct ManagerTask { packet_out_tx: mpsc::Sender, handle_allocator: dtp::TrackHandleAllocator, pending_publications: - HashMap, PublishError>>>, + HashMap>>, active_publications: HashMap>, } @@ -176,7 +175,7 @@ impl ManagerTask { Ok(()) } - fn create_local_track(&mut self, info: DataTrackInfo) -> DataTrack { + fn create_local_track(&mut self, info: DataTrackInfo) -> LocalDataTrack { let (frame_tx, frame_rx) = mpsc::channel(4); // TODO: tune let (state_tx, state_rx) = watch::channel(DataTrackState::Published); let info = Arc::new(info); @@ -195,7 +194,7 @@ impl ManagerTask { self.active_publications.insert(info.handle, state_tx.clone()); let handle = LocalTrackInner { frame_tx, state_tx }; - DataTrack::::new(info, handle) + LocalDataTrack::new(info, handle) } fn handle_request_response( diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 2052c3c76..148624929 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -35,7 +35,7 @@ pub enum InputEvent { #[derive(Debug, Clone, FromVariants)] pub enum OutputEvent { SubscriptionUpdated(SubscriptionUpdatedEvent), - /// Track has been published and a track object has been created for + /// Remote track has been published and a track object has been created for /// the user to interact with. TrackAvailable(DataTrack), } From 6a6a06d0b25e1b49399ec4d2cf1a8659aa0150b2 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:28:34 +1100 Subject: [PATCH 060/232] Format --- livekit-datatrack/src/local/manager.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 291c59964..981b809a2 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -12,15 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{ - LocalTrackInner, - pipeline::{LocalTrackTask} -}; -use crate::{LocalDataTrack, dtp::TrackHandle}; +use super::{pipeline::LocalTrackTask, LocalTrackInner}; use crate::{ - dtp, DataTrack, DataTrackInfo, DataTrackOptions, DataTrackState, EncryptionProvider, - InternalError, PublishError, + dtp, DataTrackInfo, DataTrackOptions, DataTrackState, EncryptionProvider, InternalError, + PublishError, }; +use crate::{dtp::TrackHandle, LocalDataTrack}; use anyhow::{anyhow, Context}; use bytes::Bytes; use from_variants::FromVariants; @@ -33,8 +30,6 @@ use tokio::{ }; use tokio_stream::wrappers::ReceiverStream; - - #[derive(Debug)] pub struct ManagerOptions { pub encryption: Option>, From a4664256150f8959b150df136173af7d31a313f0 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:20:07 +1100 Subject: [PATCH 061/232] Separate buffer sizes --- livekit-datatrack/src/remote/manager.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 148624929..f30fa9eaa 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -75,16 +75,14 @@ pub struct ManagerOptions { /// Manager for remote data tracks. pub struct Manager { - event_in_tx: mpsc::Sender, // sub request + event_in_tx: mpsc::Sender, } impl Manager { - const CH_BUFFER_SIZE: usize = 4; - /// Creates a new manager with the specified options. pub fn new(options: ManagerOptions) -> (Self, ManagerTask, impl Stream) { - let (event_in_tx, event_in_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); - let (event_out_tx, event_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); + let (event_in_tx, event_in_rx) = mpsc::channel(Self::INPUT_BUFFER_SIZE); + let (event_out_tx, event_out_rx) = mpsc::channel(Self::OUTPUT_BUFFER_SIZE); let manager = Manager { event_in_tx }; let task = ManagerTask { decryption: options.decryption, event_in_rx, event_out_tx }; @@ -97,6 +95,12 @@ impl Manager { pub fn handle_event(&self, event: InputEvent) -> Result<(), InternalError> { Ok(self.event_in_tx.try_send(event).context("Failed to send input event")?) } + + /// Number of [`InputEvent`]s to buffer. + const INPUT_BUFFER_SIZE: usize = 4; + + /// Number of [`OutputEvent`]s to buffer. + const OUTPUT_BUFFER_SIZE: usize = 4; } pub struct ManagerTask { From aa46af322941b747c907b7fee8e22dc9ee7d83e0 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:26:14 +1100 Subject: [PATCH 062/232] Decouple proto for local manager --- livekit-datatrack/src/common/mod.rs | 1 - livekit-datatrack/src/common/proto.rs | 31 -- livekit-datatrack/src/local/manager.rs | 359 ++++++++++-------------- livekit-datatrack/src/local/mod.rs | 1 + livekit-datatrack/src/local/pipeline.rs | 20 +- livekit-datatrack/src/local/proto.rs | 90 ++++++ 6 files changed, 255 insertions(+), 247 deletions(-) delete mode 100644 livekit-datatrack/src/common/proto.rs create mode 100644 livekit-datatrack/src/local/proto.rs diff --git a/livekit-datatrack/src/common/mod.rs b/livekit-datatrack/src/common/mod.rs index 58ee9df96..ffd2ac026 100644 --- a/livekit-datatrack/src/common/mod.rs +++ b/livekit-datatrack/src/common/mod.rs @@ -16,7 +16,6 @@ use thiserror::Error; mod e2ee; mod frame; -mod proto; mod track; pub use {e2ee::*, frame::*, track::*}; diff --git a/livekit-datatrack/src/common/proto.rs b/livekit-datatrack/src/common/proto.rs deleted file mode 100644 index da9bd104f..000000000 --- a/livekit-datatrack/src/common/proto.rs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2025 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 crate::{dtp::TrackHandle, DataTrackInfo, InternalError}; -use anyhow::anyhow; -use livekit_protocol as proto; - -impl TryInto for proto::DataTrackInfo { - type Error = InternalError; - - fn try_into(self) -> Result { - let handle: TrackHandle = self.pub_handle.try_into().map_err(anyhow::Error::from)?; - let uses_e2ee = match self.encryption() { - proto::encryption::Type::None => false, - proto::encryption::Type::Gcm => true, - other => Err(anyhow!("Unsupported E2EE type: {:?}", other))?, - }; - Ok(DataTrackInfo { handle, sid: self.sid, name: self.name, uses_e2ee }) - } -} diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 981b809a2..2a48b883a 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -22,14 +22,81 @@ use anyhow::{anyhow, Context}; use bytes::Bytes; use from_variants::FromVariants; use futures_util::Stream; -use livekit_protocol as proto; use std::{collections::HashMap, sync::Arc, time::Duration}; -use tokio::{ - sync::{mpsc, oneshot, watch}, - time::timeout, -}; +use tokio::sync::{mpsc, oneshot, watch}; +use tokio::time; use tokio_stream::wrappers::ReceiverStream; +/// An external event handled by [`Manager`]. +#[derive(Debug, FromVariants)] +pub enum InputEvent { + Publish(PublishEvent), + PublishResult(PublishResultEvent), + Unpublish(UnpublishEvent) + // TODO: add shutdown event +} + +/// An event produced by [`Manager`] requiring external action. +#[derive(Debug, FromVariants)] +pub enum OutputEvent { + PublishRequest(PublishRequestEvent), + UnpublishRequest(UnpublishRequestEvent), + /// Encoded packet is ready to be sent over the transport. + PacketAvailable(Bytes), +} + +/// Result of a publish request. +#[derive(Debug)] +pub struct PublishResultEvent { + /// Publisher handle of the track. + pub handle: TrackHandle, + /// Outcome of the publish request. + pub result: Result, +} + +/// SFU notification that a track published by the local participant +/// has been unpublished. +#[derive(Debug)] +pub struct UnpublishEvent { + /// Publisher handle of the track that was unpublished. + handle: TrackHandle, +} + +/// Local participant requested to publish a track. +#[derive(Debug)] +pub struct PublishRequestEvent { + pub handle: TrackHandle, + pub name: String, + pub uses_e2ee: bool, +} + +/// Local participant unpublished a track. +/// +/// This can either occur explicitly through user action or implicitly when the last +/// reference to the track is dropped. +/// +#[derive(Debug)] +pub struct UnpublishRequestEvent { + /// Publisher handle of the track to unpublish. + pub handle: TrackHandle, +} + +/// Request to publish a data track. +#[derive(Debug)] +pub struct PublishEvent { + /// Publish options. + options: DataTrackOptions, + /// Async completion channel. + result_tx: oneshot::Sender>, +} + +/// Request to publish a data track timed-out. +#[derive(Debug)] +pub struct PublishTimeoutEvent { + /// Publisher handle of the pending publication. + handle: TrackHandle, +} + #[derive(Debug)] pub struct ManagerOptions { pub encryption: Option>, @@ -38,47 +105,32 @@ pub struct ManagerOptions { /// Manager for local data tracks. #[derive(Debug, Clone)] pub struct Manager { - signal_in_tx: mpsc::Sender, - pub_req_tx: mpsc::Sender, + event_in_tx: mpsc::Sender, } impl Manager { - const CH_BUFFER_SIZE: usize = 4; - const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); - - pub fn new( - options: ManagerOptions, - ) -> (Self, ManagerTask, impl Stream, impl Stream) { - let (pub_req_tx, pub_req_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); - let (signal_in_tx, signal_in_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); - let (signal_out_tx, signal_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); - let (packet_out_tx, packet_out_rx) = mpsc::channel(Self::CH_BUFFER_SIZE); + pub fn new(options: ManagerOptions) -> (Self, ManagerTask, impl Stream) { + let (event_in_tx, event_in_rx) = mpsc::channel(Self::INPUT_BUFFER_SIZE); + let (event_out_tx, signal_out_rx) = mpsc::channel(Self::OUTPUT_BUFFER_SIZE); - let manager = Self { signal_in_tx, pub_req_tx }; + let manager = Self { event_in_tx: event_in_tx.clone() }; let task = ManagerTask { encryption: options.encryption, - pub_req_rx, - signal_in_rx, - signal_out_tx, - packet_out_tx, + event_in_tx: event_in_tx.downgrade(), + event_in_rx, + event_out_tx, handle_allocator: dtp::TrackHandleAllocator::default(), pending_publications: HashMap::new(), active_publications: HashMap::new(), }; - let signal_out_stream = ReceiverStream::new(signal_out_rx); - let packet_out_stream = ReceiverStream::new(packet_out_rx); - - (manager, task, signal_out_stream, packet_out_stream) + let event_out_stream = ReceiverStream::new(signal_out_rx); + (manager, task, event_out_stream) } - /// Handles a signal message from the SFU. - /// - /// In order to function correctly, all message types enumerated in [`PubSignalInput`] - /// must be forwarded here. - /// - pub fn handle_signal(&self, message: PubSignalInput) -> Result<(), InternalError> { - Ok(self.signal_in_tx.try_send(message).context("Failed to handle signal input")?) + /// Handles an external event. + pub fn handle_event(&self, event: InputEvent) -> Result<(), InternalError> { + Ok(self.event_in_tx.try_send(event).context("Failed to handle input event")?) } /// Publishes a data track with the given options. @@ -87,29 +139,26 @@ impl Manager { options: DataTrackOptions, ) -> Result { let (result_tx, result_rx) = oneshot::channel(); - let request = PubRequest { options, result_tx }; - self.pub_req_tx.try_send(request).map_err(|_| PublishError::Disconnected)?; - - // TODO: move timeout inside pub manager - let track = timeout(Self::PUBLISH_TIMEOUT, result_rx) - .await - .map_err(|_| PublishError::Timeout)? - .map_err(|_| PublishError::Disconnected)??; + let event = PublishEvent { options, result_tx }; + + self.event_in_tx.try_send(event.into()).map_err(|_| PublishError::Disconnected)?; + let track = result_rx.await.map_err(|_| PublishError::Disconnected)??; + Ok(track) } -} -struct PubRequest { - options: DataTrackOptions, - result_tx: oneshot::Sender>, + /// Number of [`InputEvent`]s to buffer. + const INPUT_BUFFER_SIZE: usize = 4; + + /// Number of [`OutputEvent`]s to buffer. + const OUTPUT_BUFFER_SIZE: usize = 4; } pub struct ManagerTask { encryption: Option>, - pub_req_rx: mpsc::Receiver, - signal_in_rx: mpsc::Receiver, - signal_out_tx: mpsc::Sender, - packet_out_tx: mpsc::Sender, + event_in_tx: mpsc::WeakSender, + event_in_rx: mpsc::Receiver, + event_out_tx: mpsc::Sender, handle_allocator: dtp::TrackHandleAllocator, pending_publications: HashMap>>, @@ -117,56 +166,66 @@ pub struct ManagerTask { } impl ManagerTask { - pub async fn run(mut self) -> Result<(), InternalError> { - loop { - tokio::select! { - biased; // Handle signal input before publish requests. - // TODO: check cancellation - Some(signal) = self.signal_in_rx.recv() => self.handle_signal(signal), - //Some(unpublish_req) = self.unpub_req_rx.recv() => self.handle_unpublish_req(unpublish_req), - Some(publish_req) = self.pub_req_rx.recv() => self.handle_publish_req(publish_req), - else => Ok(()) - } - .inspect_err(|err| log::error!("{}", err)) - .ok(); + pub async fn run(mut self) { + // TODO: check cancellation + while let Some(event) = self.event_in_rx.recv().await { + let Err(err) = self.handle_event(event) else { continue }; + log::error!("Failed to handle input event: {}", err); } } - fn handle_publish_req(&mut self, req: PubRequest) -> Result<(), InternalError> { + fn handle_event(&mut self, event: InputEvent) -> Result<(), InternalError> { + match event { + InputEvent::Publish(event) => self.handle_publish_request(event), + InputEvent::PublishResult(event) => self.handle_publish_result(event), + InputEvent::Unpublish(event) => self.handle_unpublished(event), + } + } + + fn handle_publish_request(&mut self, event: PublishEvent) -> Result<(), InternalError> { let Some(handle) = self.handle_allocator.get() else { - _ = req.result_tx.send(Err(PublishError::LimitReached)); + _ = event.result_tx.send(Err(PublishError::LimitReached)); return Ok(()); }; - if self.pending_publications.insert(handle, req.result_tx).is_some() { - Err(anyhow!("Publication already pending for handle"))? + if self.pending_publications.contains_key(&handle) { + _ = event.result_tx.send(Err(PublishError::Internal( + anyhow!("Publication already pending for handle").into(), + ))); + return Ok(()); } + self.pending_publications.insert(handle, event.result_tx); - let use_e2ee = self.encryption.is_some() && !req.options.disable_e2ee; - let request = req.options.into_add_track_request(use_e2ee, handle); - self.signal_out_tx.try_send(request.into()).context("Failed to send add track")?; + let publish_requested = PublishRequestEvent { + handle, + name: event.options.name, + uses_e2ee: self.encryption.is_some() && !event.options.disable_e2ee, + }; + _ = self.event_out_tx.try_send(publish_requested.into()); // TODO: check for error. + self.schedule_publish_timeout(handle); Ok(()) } - fn handle_signal(&mut self, message: PubSignalInput) -> Result<(), InternalError> { - match message { - PubSignalInput::PublishResponse(res) => self.handle_publish_response(res), - PubSignalInput::UnpublishResponse(res) => self.handle_unpublish_response(res), - PubSignalInput::RequestResponse(res) => self.handle_request_response(res), - PubSignalInput::SyncState(res) => self.handle_sync_state(res), - } + fn schedule_publish_timeout(&self, handle: TrackHandle) { + let event_in_tx = self.event_in_tx.clone(); + let emit_timeout = async move { + time::sleep(Self::PUBLISH_TIMEOUT).await; + let Some(tx) = event_in_tx.upgrade() else { return }; + let event = PublishResultEvent { handle, result: Err(PublishError::Timeout) }; + _ = tx.try_send(event.into()) + }; + livekit_runtime::spawn(emit_timeout); } - fn handle_publish_response( - &mut self, - res: proto::PublishDataTrackResponse, - ) -> Result<(), InternalError> { - let info: DataTrackInfo = res.try_into()?; - let Some(res_tx) = self.pending_publications.remove(&info.handle) else { - Err(anyhow!("No pending track publication for {}", info.handle))? + fn handle_publish_result(&mut self, event: PublishResultEvent) -> Result<(), InternalError> { + let Some(result_tx) = self.pending_publications.remove(&event.handle) else { + Err(anyhow!("No pending track publication for {}", event.handle))? }; - let track = self.create_local_track(info); - let _ = res_tx.send(Ok(track)); + if result_tx.is_closed() { + return Ok(()); + } + let result = event.result.map(|track_info| self.create_local_track(track_info)); + _ = result_tx.send(result); Ok(()) } @@ -177,57 +236,23 @@ impl ManagerTask { let task = LocalTrackTask { // TODO: handle cancellation - packetizer: dtp::Packetizer::new(info.handle, 16_000), + packetizer: dtp::Packetizer::new(info.handle, Self::TRANSPORT_MTU), encryption: self.encryption.clone(), info: info.clone(), frame_rx, state_rx, - packet_out_tx: self.packet_out_tx.clone(), - signal_out_tx: self.signal_out_tx.clone(), + event_out_tx: self.event_out_tx.clone(), }; livekit_runtime::spawn(task.run()); self.active_publications.insert(info.handle, state_tx.clone()); - let handle = LocalTrackInner { frame_tx, state_tx }; - LocalDataTrack::new(info, handle) - } - - fn handle_request_response( - &mut self, - res: proto::RequestResponse, - ) -> Result<(), InternalError> { - let reason = res.reason(); - let req = res.request.context("Missing request")?; - use proto::request_response::Request; - match req { - Request::PublishDataTrack(req) => { - let handle: TrackHandle = - req.pub_handle.try_into().context("Invalid track handle")?; - let Some(res_tx) = self.pending_publications.remove(&handle) else { - Err(anyhow!("No pending publication for {}", req.pub_handle))? - }; - let error: PublishError = reason.into(); - _ = res_tx.send(Err(error)); - } - Request::UnpublishDataTrack(req) => { - log::warn!("Unpublish failed for {}", req.pub_handle) - } - _ => {} // Not handled by this module - } - Ok(()) + let inner = LocalTrackInner { frame_tx, state_tx }; + LocalDataTrack::new(info, inner) } - fn handle_unpublish_response( - &mut self, - res: proto::UnpublishDataTrackResponse, - ) -> Result<(), InternalError> { - let handle = { - let info: DataTrackInfo = - res.info.context("Missing info")?.try_into().context("Invalid info")?; - info.handle - }; - let Some(state_tx) = self.active_publications.remove(&handle) else { - Err(anyhow!("Cannot handle unpublish for unknown track {}", handle))? + fn handle_unpublished(&mut self, event: UnpublishEvent) -> Result<(), InternalError> { + let Some(state_tx) = self.active_publications.remove(&event.handle) else { + Err(anyhow!("Cannot handle unpublish for unknown track {}", event.handle))? }; let state = *state_tx.borrow(); match state { @@ -245,83 +270,9 @@ impl ManagerTask { Ok(()) } - fn handle_sync_state(&mut self, res: proto::SyncState) -> Result<(), InternalError> { - for res in res.publish_data_tracks { - // Forward to standard response handler - self.handle_publish_response(res)? - } - Ok(()) - } -} - -#[derive(Debug, FromVariants)] -pub enum PubManagerInput { - Signal(PubSignalInput), - Transport(Bytes), -} - -/// Signal message produced by [`PubManager`] to be forwarded to the SFU. -#[derive(Debug, FromVariants)] -pub enum PubSignalOutput { - PublishRequest(proto::PublishDataTrackRequest), - UnpublishRequest(proto::UnpublishDataTrackRequest), -} - -/// Signal message received from the SFU handled by [`PubManager`]. -#[derive(Debug, FromVariants)] -pub enum PubSignalInput { - PublishResponse(proto::PublishDataTrackResponse), - UnpublishResponse(proto::UnpublishDataTrackResponse), - RequestResponse(proto::RequestResponse), - SyncState(proto::SyncState), -} - -impl DataTrackOptions { - fn into_add_track_request( - self, - use_e2ee: bool, - handle: TrackHandle, - ) -> proto::PublishDataTrackRequest { - let encryption = if self.disable_e2ee || !use_e2ee { - proto::encryption::Type::None - } else { - proto::encryption::Type::Gcm - }; - proto::PublishDataTrackRequest { - pub_handle: handle.into(), - name: self.name, - encryption: encryption.into(), - } - } -} - -impl From for PublishError { - fn from(reason: proto::request_response::Reason) -> Self { - use proto::request_response::Reason; - // If new error cases are added in the future, consider if they should - // be treated as internal errors or added to the public error enum. - match reason { - Reason::NotAllowed => PublishError::NotAllowed, - Reason::DuplicateName => PublishError::DuplicateName, - other => PublishError::Internal(anyhow!("SFU rejected: {:?}", other).into()), - } - } -} - -impl TryInto for proto::PublishDataTrackResponse { - type Error = InternalError; - fn try_into(self) -> Result { - let info = self.info.context("Missing info")?; - info.try_into() - } -} + /// How long to wait for an SFU response for a track publication before timeout. + const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); -impl From for proto::signal_request::Message { - fn from(output: PubSignalOutput) -> Self { - use proto::signal_request::Message; - match output { - PubSignalOutput::PublishRequest(req) => Message::PublishDataTrackRequest(req), - PubSignalOutput::UnpublishRequest(req) => Message::UnpublishDataTrackRequest(req), - } - } -} + /// MTU of the transport + const TRANSPORT_MTU: usize = 16_000; +} \ No newline at end of file diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 49fef4e62..e4343a430 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -21,6 +21,7 @@ use tokio::sync::{mpsc, watch}; mod manager; mod pipeline; +mod proto; /// Data track published by the local participant. pub type LocalDataTrack = DataTrack; diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 436a3e432..ab20d8760 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::manager::PubSignalOutput; use crate::{ - dtp, DataTrackFrame, DataTrackInfo, DataTrackState, EncryptionProvider, InternalError, + dtp, + local::manager::{OutputEvent, UnpublishRequestEvent}, + DataTrackFrame, DataTrackInfo, DataTrackState, EncryptionProvider, InternalError, }; use anyhow::Context; -use bytes::Bytes; -use livekit_protocol as proto; use std::sync::Arc; use tokio::sync::{mpsc, watch}; @@ -29,8 +28,7 @@ pub(super) struct LocalTrackTask { pub info: Arc, pub state_rx: watch::Receiver, pub frame_rx: mpsc::Receiver, - pub packet_out_tx: mpsc::Sender, - pub signal_out_tx: mpsc::Sender, + pub event_out_tx: mpsc::Sender, } impl LocalTrackTask { @@ -50,7 +48,7 @@ impl LocalTrackTask { } if let DataTrackState::Unpublished { sfu_initiated } = state { if !sfu_initiated { - self.send_unpublish_req()?; + self.send_local_unpublish()?; } } Ok(()) @@ -77,13 +75,13 @@ impl LocalTrackTask { let packets = self.packetizer.packetize(frame).context("Failed to packetize frame")?; for packet in packets { let serialized = packet.serialize(); - self.packet_out_tx.try_send(serialized).context("Failed to send packet")?; + self.event_out_tx.try_send(serialized.into()).context("Failed to send packet")?; } Ok(()) } - fn send_unpublish_req(self) -> Result<(), InternalError> { - let req = proto::UnpublishDataTrackRequest { pub_handle: self.info.handle.into() }; - Ok(self.signal_out_tx.try_send(req.into()).context("Failed to send unpublish")?) + fn send_local_unpublish(self) -> Result<(), InternalError> { + let event = UnpublishRequestEvent { handle: self.info.handle }; + Ok(self.event_out_tx.try_send(event.into()).context("Failed to send unpublish")?) } } diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs new file mode 100644 index 000000000..621e4b311 --- /dev/null +++ b/livekit-datatrack/src/local/proto.rs @@ -0,0 +1,90 @@ +// Copyright 2025 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 super::manager::{PublishRequestEvent, UnpublishRequestEvent}; +use crate::{ + dtp::TrackHandle, local::manager::PublishResultEvent, DataTrackInfo, InternalError, + PublishError, +}; +use anyhow::{anyhow, Context}; +use livekit_protocol as proto; +use std::mem; + +// MARK: - Output event -> protocol + +impl From for proto::PublishDataTrackRequest { + fn from(event: PublishRequestEvent) -> 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 } + } +} + +impl From for proto::UnpublishDataTrackRequest { + fn from(event: UnpublishRequestEvent) -> Self { + Self { pub_handle: event.handle.into() } + } +} + +// MARK: - Protocol -> input event + +impl TryFrom for PublishResultEvent { + type Error = InternalError; + + fn try_from(msg: proto::PublishDataTrackResponse) -> Result { + let info: DataTrackInfo = msg.info.context("Missing info")?.try_into()?; + Ok(Self { handle: info.handle, result: Ok(info) }) + } +} + +impl TryFrom for DataTrackInfo { + type Error = InternalError; + + fn try_from(msg: proto::DataTrackInfo) -> Result { + let handle: TrackHandle = msg.pub_handle.try_into().map_err(anyhow::Error::from)?; + let uses_e2ee = match msg.encryption() { + proto::encryption::Type::None => false, + proto::encryption::Type::Gcm => true, + other => Err(anyhow!("Unsupported E2EE type: {:?}", other))?, + }; + Ok(Self { handle, sid: msg.sid, name: msg.name, uses_e2ee }) + } +} + +fn publish_result_from_request_response( + msg: &proto::RequestResponse, +) -> Option { + use proto::request_response::{Reason, Request}; + let Some(request) = &msg.request else { return None }; + let Request::PublishDataTrack(request) = request else { return None }; + let Ok(handle) = TryInto::::try_into(request.pub_handle) else { return None }; + let error = match msg.reason() { + // If new error reasons are introduced in the future, consider adding them + // to the public error enum if they are useful to the user. + Reason::NotAllowed => PublishError::NotAllowed, + Reason::DuplicateName => PublishError::DuplicateName, + _ => PublishError::Internal(anyhow!("SFU rejected: {}", msg.message).into()), + }; + let event = PublishResultEvent { handle, result: Err(error) }; + Some(event) +} + +fn publish_results_from_sync_state( + msg: &mut proto::SyncState, +) -> Result, InternalError> { + mem::take(&mut msg.publish_data_tracks) + .into_iter() + .map(TryInto::::try_into) + .collect::, InternalError>>() +} From 267db6b1dc59f9fd04396d75af1d143be600dce3 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 30 Dec 2025 12:03:25 +1100 Subject: [PATCH 063/232] Protocol conversion unit tests --- livekit-datatrack/src/local/proto.rs | 95 +++++++++++++++++++++++++++ livekit-datatrack/src/remote/proto.rs | 51 ++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index 621e4b311..8a50b7dde 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -88,3 +88,98 @@ fn publish_results_from_sync_state( .map(TryInto::::try_into) .collect::, InternalError>>() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_publish_request_event() { + let event = PublishRequestEvent { + handle: 1u32.try_into().unwrap(), + name: "track".into(), + uses_e2ee: true, + }; + let request: proto::PublishDataTrackRequest = event.into(); + assert_eq!(request.pub_handle, 1); + assert_eq!(request.name, "track"); + assert_eq!(request.encryption(), proto::encryption::Type::Gcm); + } + + #[test] + fn test_from_unpublish_request_event() { + let event = UnpublishRequestEvent { handle: 1u32.try_into().unwrap() }; + let request: proto::UnpublishDataTrackRequest = event.into(); + assert_eq!(request.pub_handle, 1); + } + + #[test] + fn test_from_publish_response() { + let response = proto::PublishDataTrackResponse { + info: proto::DataTrackInfo { + pub_handle: 1, + sid: "DTR_1234".into(), + name: "track".into(), + encryption: proto::encryption::Type::Gcm.into(), + } + .into(), + }; + let event: PublishResultEvent = response.try_into().unwrap(); + assert_eq!(event.handle, 1u32.try_into().unwrap()); + + let info = event.result.expect("Expected ok result"); + assert_eq!(info.handle, 1u32.try_into().unwrap()); + assert_eq!(info.sid, "DTR_1234"); + assert_eq!(info.name, "track"); + assert!(info.uses_e2ee); + } + + #[test] + fn test_from_request_response() { + use proto::request_response::{Reason, Request}; + let response = proto::RequestResponse { + request: Request::PublishDataTrack(proto::PublishDataTrackRequest { + pub_handle: 1, + ..Default::default() + }) + .into(), + reason: Reason::NotAllowed.into(), + ..Default::default() + }; + + let event = publish_result_from_request_response(&response).expect("Expected event"); + assert_eq!(event.handle, 1u32.try_into().unwrap()); + assert!(matches!(event.result, Err(PublishError::NotAllowed))); + } + + #[test] + fn test_from_sync_state() { + let mut sync_state = proto::SyncState { + publish_data_tracks: vec![ + proto::PublishDataTrackResponse { + info: proto::DataTrackInfo { + pub_handle: 1, + sid: "DTR_1234".into(), + name: "track1".into(), + encryption: proto::encryption::Type::Gcm.into(), + } + .into(), + }, + proto::PublishDataTrackResponse { + info: proto::DataTrackInfo { + pub_handle: 2, + sid: "DTR_4567".into(), + name: "track2".into(), + encryption: proto::encryption::Type::Gcm.into(), + } + .into(), + }, + ], + ..Default::default() + }; + let events = publish_results_from_sync_state(&mut sync_state).expect("Expected events"); + assert_eq!(events.len(), 2); + assert!(sync_state.publish_data_tracks.is_empty(), "Expected original vec taken"); + assert!(events.into_iter().all(|event| event.result.is_ok())); + } +} diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index bb9a2dca2..f569c0261 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -88,3 +88,54 @@ impl From for proto::UpdateDataSubscription { Self { updates: vec![update] } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_subscriber_handles() { + let sub_handles = [ + ( + 1, + proto::data_track_subscriber_handles::PublishedDataTrack { + track_sid: "DTR_1234".into(), + ..Default::default() + }, + ), + ( + 2, + proto::data_track_subscriber_handles::PublishedDataTrack { + track_sid: "DTR_4567".into(), + ..Default::default() + }, + ), + ]; + let subscriber_handles = + proto::DataTrackSubscriberHandles { sub_handles: HashMap::from(sub_handles) }; + + let event: SubscriberHandlesEvent = subscriber_handles.try_into().unwrap(); + assert_eq!(event.mapping.get(&1u32.try_into().unwrap()).unwrap(), "DTR_1234"); + assert_eq!(event.mapping.get(&2u32.try_into().unwrap()).unwrap(), "DTR_4567"); + } + + #[test] + fn test_extract_track_info() { + let data_tracks = vec![proto::DataTrackInfo { + pub_handle: 1, + sid: "DTR_1234".into(), + name: "track1".into(), + encryption: proto::encryption::Type::Gcm.into(), + }]; + let mut participant_info = proto::ParticipantInfo { data_tracks, ..Default::default() }; + + let track_info = extract_track_info(&mut participant_info).unwrap(); + assert!(participant_info.data_tracks.is_empty(), "Expected original vec taken"); + assert_eq!(track_info.len(), 1); + + let first = track_info.first().unwrap(); + assert_eq!(first.handle, 1u32.try_into().unwrap()); + assert_eq!(first.name, "track1"); + assert_eq!(first.sid, "DTR_1234"); + } +} From bf98cc5965fcda68a6628b976bde503ead3ec844 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 30 Dec 2025 12:40:15 +1100 Subject: [PATCH 064/232] Shutdown event for local manager --- livekit-datatrack/src/common/track.rs | 6 --- livekit-datatrack/src/local/manager.rs | 68 ++++++++++++++++++------- livekit-datatrack/src/local/mod.rs | 8 +-- livekit-datatrack/src/local/pipeline.rs | 34 +++++-------- 4 files changed, 66 insertions(+), 50 deletions(-) diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs index ad55c03e2..2fa275212 100644 --- a/livekit-datatrack/src/common/track.rs +++ b/livekit-datatrack/src/common/track.rs @@ -25,12 +25,6 @@ pub struct DataTrackInfo { pub(crate) uses_e2ee: bool, } -#[derive(Debug, Clone, Copy)] -pub(crate) enum DataTrackState { - Published, - Unpublished { sfu_initiated: bool }, -} - impl DataTrackInfo { /// Unique track identifier. pub fn sid(&self) -> &str { diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 2a48b883a..016030491 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -14,8 +14,7 @@ use super::{pipeline::LocalTrackTask, LocalTrackInner}; use crate::{ - dtp, DataTrackInfo, DataTrackOptions, DataTrackState, EncryptionProvider, InternalError, - PublishError, + dtp, DataTrackInfo, DataTrackOptions, EncryptionProvider, InternalError, PublishError, }; use crate::{dtp::TrackHandle, LocalDataTrack}; use anyhow::{anyhow, Context}; @@ -32,8 +31,9 @@ use tokio_stream::wrappers::ReceiverStream; pub enum InputEvent { Publish(PublishEvent), PublishResult(PublishResultEvent), - Unpublish(UnpublishEvent) - // TODO: add shutdown event + Unpublish(UnpublishEvent), + /// Shutdown the manager and all associated tracks. + Shutdown, } /// An event produced by [`Manager`] requiring external action. @@ -97,6 +97,25 @@ pub struct PublishTimeoutEvent { handle: TrackHandle, } +#[derive(Debug, Clone, Copy)] +pub(crate) enum UnpublishInitiator { + Client, + Sfu, + Shutdown, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum LocalTrackState { + Published, + Unpublished { initiator: UnpublishInitiator }, +} + +impl LocalTrackState { + pub fn is_published(&self) -> bool { + matches!(self, Self::Published) + } +} + #[derive(Debug)] pub struct ManagerOptions { pub encryption: Option>, @@ -162,16 +181,19 @@ pub struct ManagerTask { handle_allocator: dtp::TrackHandleAllocator, pending_publications: HashMap>>, - active_publications: HashMap>, + active_publications: HashMap>, } impl ManagerTask { pub async fn run(mut self) { - // TODO: check cancellation while let Some(event) = self.event_in_rx.recv().await { + if matches!(event, InputEvent::Shutdown) { + break; + } let Err(err) = self.handle_event(event) else { continue }; log::error!("Failed to handle input event: {}", err); } + self.shutdown(); } fn handle_event(&mut self, event: InputEvent) -> Result<(), InternalError> { @@ -179,6 +201,7 @@ impl ManagerTask { InputEvent::Publish(event) => self.handle_publish_request(event), InputEvent::PublishResult(event) => self.handle_publish_result(event), InputEvent::Unpublish(event) => self.handle_unpublished(event), + _ => Ok(()), } } @@ -231,7 +254,7 @@ impl ManagerTask { fn create_local_track(&mut self, info: DataTrackInfo) -> LocalDataTrack { let (frame_tx, frame_rx) = mpsc::channel(4); // TODO: tune - let (state_tx, state_rx) = watch::channel(DataTrackState::Published); + let (state_tx, state_rx) = watch::channel(LocalTrackState::Published); let info = Arc::new(info); let task = LocalTrackTask { @@ -255,24 +278,31 @@ impl ManagerTask { Err(anyhow!("Cannot handle unpublish for unknown track {}", event.handle))? }; let state = *state_tx.borrow(); - match state { - DataTrackState::Published => { - state_tx - .send(DataTrackState::Unpublished { sfu_initiated: true }) - .context("Failed to set state")?; - } - DataTrackState::Unpublished { sfu_initiated } => { - if sfu_initiated { - Err(anyhow!("Received unpublish response for same track more than once"))? - } - } + if !matches!(state, LocalTrackState::Published) { + return Ok(()); } + state_tx + .send(LocalTrackState::Unpublished { initiator: UnpublishInitiator::Sfu }) + .context("Failed to set state")?; Ok(()) } + /// Performs cleanup before the task ends. + fn shutdown(self) { + // Resolve any pending track publications + for (_, result_tx) in self.pending_publications { + _ = result_tx.send(Err(PublishError::Disconnected)); + } + // Mark all active publications as unpublished + for (_, state_tx) in self.active_publications { + _ = state_tx + .send(LocalTrackState::Unpublished { initiator: UnpublishInitiator::Shutdown }); + } + } + /// How long to wait for an SFU response for a track publication before timeout. const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); /// MTU of the transport const TRANSPORT_MTU: usize = 16_000; -} \ No newline at end of file +} diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index e4343a430..e213cab46 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::{ - DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, DataTrackState, InternalError, + DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, InternalError, local::manager::{LocalTrackState, UnpublishInitiator}, }; use std::{fmt, marker::PhantomData, sync::Arc}; use thiserror::Error; @@ -57,7 +57,7 @@ impl DataTrack { /// Whether or not the track is still published. pub fn is_published(&self) -> bool { - matches!(*self.inner().state_tx.borrow(), DataTrackState::Published) + self.inner().state_tx.borrow().is_published() } /// Unpublish the track. @@ -69,13 +69,13 @@ impl DataTrack { #[derive(Debug, Clone)] pub(crate) struct LocalTrackInner { pub frame_tx: mpsc::Sender, - pub state_tx: watch::Sender, + pub state_tx: watch::Sender, } impl LocalTrackInner { fn local_unpublish(&self) { self.state_tx - .send(DataTrackState::Unpublished { sfu_initiated: false }) + .send(LocalTrackState::Unpublished { initiator: UnpublishInitiator::Client }) .inspect_err(|err| log::error!("Failed to update state to unsubscribed: {err}")) .ok(); } diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index ab20d8760..175f0e059 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -14,8 +14,8 @@ use crate::{ dtp, - local::manager::{OutputEvent, UnpublishRequestEvent}, - DataTrackFrame, DataTrackInfo, DataTrackState, EncryptionProvider, InternalError, + local::manager::{LocalTrackState, OutputEvent, UnpublishInitiator, UnpublishRequestEvent}, + DataTrackFrame, DataTrackInfo, EncryptionProvider, InternalError, }; use anyhow::Context; use std::sync::Arc; @@ -26,32 +26,29 @@ pub(super) struct LocalTrackTask { pub packetizer: dtp::Packetizer, pub encryption: Option>, pub info: Arc, - pub state_rx: watch::Receiver, + pub state_rx: watch::Receiver, pub frame_rx: mpsc::Receiver, pub event_out_tx: mpsc::Sender, } impl LocalTrackTask { - pub async fn run(mut self) -> Result<(), InternalError> { - let mut state = DataTrackState::Published; - while matches!(state, DataTrackState::Published) { + pub async fn run(mut self) { + let mut state = *self.state_rx.borrow(); + while state.is_published() { tokio::select! { _ = self.state_rx.changed() => { - let _: () = state = *self.state_rx.borrow(); - Ok(()) + state = *self.state_rx.borrow(); + }, + Some(frame) = self.frame_rx.recv() => { + _ = self.publish_frame(frame).inspect_err(|err| log::error!("{}", err)); }, - Some(frame) = self.frame_rx.recv() => self.publish_frame(frame), else => break } - .inspect_err(|err| log::error!("{}", err)) - .ok(); } - if let DataTrackState::Unpublished { sfu_initiated } = state { - if !sfu_initiated { - self.send_local_unpublish()?; - } + if let LocalTrackState::Unpublished { initiator: UnpublishInitiator::Client } = state { + let event = UnpublishRequestEvent { handle: self.info.handle }; + _ = self.event_out_tx.try_send(event.into()); } - Ok(()) } fn publish_frame(&mut self, mut frame: DataTrackFrame) -> Result<(), InternalError> { @@ -79,9 +76,4 @@ impl LocalTrackTask { } Ok(()) } - - fn send_local_unpublish(self) -> Result<(), InternalError> { - let event = UnpublishRequestEvent { handle: self.info.handle }; - Ok(self.event_out_tx.try_send(event.into()).context("Failed to send unpublish")?) - } } From ae7dc676d80fa708ad68fae52742af2f8cba5655 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 30 Dec 2025 12:40:59 +1100 Subject: [PATCH 065/232] Rename --- livekit-datatrack/src/local/manager.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 016030491..c53483711 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -198,14 +198,14 @@ impl ManagerTask { fn handle_event(&mut self, event: InputEvent) -> Result<(), InternalError> { match event { - InputEvent::Publish(event) => self.handle_publish_request(event), + InputEvent::Publish(event) => self.handle_publish(event), InputEvent::PublishResult(event) => self.handle_publish_result(event), InputEvent::Unpublish(event) => self.handle_unpublished(event), _ => Ok(()), } } - fn handle_publish_request(&mut self, event: PublishEvent) -> Result<(), InternalError> { + fn handle_publish(&mut self, event: PublishEvent) -> Result<(), InternalError> { let Some(handle) = self.handle_allocator.get() else { _ = event.result_tx.send(Err(PublishError::LimitReached)); return Ok(()); From 80654cd3206678853cd3d146a3caeb7a7b9ea6c9 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 30 Dec 2025 12:42:52 +1100 Subject: [PATCH 066/232] Shutdown event for remote manager --- livekit-datatrack/src/remote/manager.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index f30fa9eaa..13542c09f 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -29,6 +29,8 @@ pub enum InputEvent { SubscriberHandles(SubscriberHandlesEvent), /// Packet has been received over the transport. PacketReceived(Bytes), + /// Shutdown the manager, ending any subscriptions. + Shutdown, } /// An event produced by [`Manager`] requiring external action. @@ -111,8 +113,10 @@ pub struct ManagerTask { impl ManagerTask { pub async fn run(mut self) { - // TODO: check cancellation while let Some(event) = self.event_in_rx.recv().await { + if matches!(event, InputEvent::Shutdown) { + break; + } let Err(err) = self.handle_event(event) else { continue }; log::error!("Failed to handle input event: {}", err); } @@ -123,6 +127,7 @@ impl ManagerTask { InputEvent::PublicationsUpdated(event) => self.handle_publications_updated(event), InputEvent::SubscriberHandles(event) => self.handle_subscriber_handles(event), InputEvent::PacketReceived(bytes) => self.handle_packet_received(bytes), + _ => Ok(()) } } From 5ce512c70bd291ca587851778b6348d9093d508a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 30 Dec 2025 13:18:11 +1100 Subject: [PATCH 067/232] Unified descriptor map --- livekit-datatrack/src/local/manager.rs | 64 +++++++++++++++++--------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index c53483711..fc5758177 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -139,8 +139,7 @@ impl Manager { event_in_rx, event_out_tx, handle_allocator: dtp::TrackHandleAllocator::default(), - pending_publications: HashMap::new(), - active_publications: HashMap::new(), + descriptors: HashMap::new(), }; let event_out_stream = ReceiverStream::new(signal_out_rx); @@ -173,15 +172,28 @@ impl Manager { const OUTPUT_BUFFER_SIZE: usize = 4; } +#[derive(Debug)] +enum Descriptor { + /// Publication is awaiting SFU response. + /// + /// The associated channel is used to send a result to the user, + /// either the local track or a publish error. + /// + Pending(oneshot::Sender>), + /// Publication is active. + /// + /// The associated channel is used to send state updates to the track's task. + /// + Active(watch::Sender), +} + pub struct ManagerTask { encryption: Option>, event_in_tx: mpsc::WeakSender, event_in_rx: mpsc::Receiver, event_out_tx: mpsc::Sender, handle_allocator: dtp::TrackHandleAllocator, - pending_publications: - HashMap>>, - active_publications: HashMap>, + descriptors: HashMap, } impl ManagerTask { @@ -211,13 +223,13 @@ impl ManagerTask { return Ok(()); }; - if self.pending_publications.contains_key(&handle) { + if self.descriptors.contains_key(&handle) { _ = event.result_tx.send(Err(PublishError::Internal( - anyhow!("Publication already pending for handle").into(), + anyhow!("Descriptor for handle already exists").into(), ))); return Ok(()); } - self.pending_publications.insert(handle, event.result_tx); + self.descriptors.insert(handle, Descriptor::Pending(event.result_tx)); let publish_requested = PublishRequestEvent { handle, @@ -241,9 +253,13 @@ impl ManagerTask { } fn handle_publish_result(&mut self, event: PublishResultEvent) -> Result<(), InternalError> { - let Some(result_tx) = self.pending_publications.remove(&event.handle) else { - Err(anyhow!("No pending track publication for {}", event.handle))? + let Some(descriptor) = self.descriptors.remove(&event.handle) else { + Err(anyhow!("No descriptor for {}", event.handle))? + }; + let Descriptor::Pending(result_tx) = descriptor else { + Err(anyhow!("Track {} already active", event.handle))? }; + if result_tx.is_closed() { return Ok(()); } @@ -267,15 +283,18 @@ impl ManagerTask { event_out_tx: self.event_out_tx.clone(), }; livekit_runtime::spawn(task.run()); - self.active_publications.insert(info.handle, state_tx.clone()); + self.descriptors.insert(info.handle, Descriptor::Active(state_tx.clone())); let inner = LocalTrackInner { frame_tx, state_tx }; LocalDataTrack::new(info, inner) } fn handle_unpublished(&mut self, event: UnpublishEvent) -> Result<(), InternalError> { - let Some(state_tx) = self.active_publications.remove(&event.handle) else { - Err(anyhow!("Cannot handle unpublish for unknown track {}", event.handle))? + let Some(descriptor) = self.descriptors.remove(&event.handle) else { + Err(anyhow!("No descriptor for track {}", event.handle))? + }; + let Descriptor::Active(state_tx) = descriptor else { + Err(anyhow!("Cannot unpublish pending track {}", event.handle))? }; let state = *state_tx.borrow(); if !matches!(state, LocalTrackState::Published) { @@ -289,14 +308,17 @@ impl ManagerTask { /// Performs cleanup before the task ends. fn shutdown(self) { - // Resolve any pending track publications - for (_, result_tx) in self.pending_publications { - _ = result_tx.send(Err(PublishError::Disconnected)); - } - // Mark all active publications as unpublished - for (_, state_tx) in self.active_publications { - _ = state_tx - .send(LocalTrackState::Unpublished { initiator: UnpublishInitiator::Shutdown }); + for (_, descriptor) in self.descriptors { + match descriptor { + Descriptor::Pending(result_tx) => { + _ = result_tx.send(Err(PublishError::Disconnected)) + } + Descriptor::Active(state_tx) => { + _ = state_tx.send(LocalTrackState::Unpublished { + initiator: UnpublishInitiator::Shutdown, + }) + } + } } } From 6e35c86331ba58dac27db0b7bdde23df84c254d0 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 30 Dec 2025 13:49:22 +1100 Subject: [PATCH 068/232] Use is_published --- livekit-datatrack/src/local/manager.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index fc5758177..4bc91bdd0 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -296,8 +296,7 @@ impl ManagerTask { let Descriptor::Active(state_tx) = descriptor else { Err(anyhow!("Cannot unpublish pending track {}", event.handle))? }; - let state = *state_tx.borrow(); - if !matches!(state, LocalTrackState::Published) { + if !state_tx.borrow().is_published() { return Ok(()); } state_tx From 016da09f71b1c92acb46a983232107491c427ca2 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:57:27 +1100 Subject: [PATCH 069/232] Biased select --- livekit-datatrack/src/local/pipeline.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 175f0e059..abbf40cce 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -36,6 +36,7 @@ impl LocalTrackTask { let mut state = *self.state_rx.borrow(); while state.is_published() { tokio::select! { + biased; _ = self.state_rx.changed() => { state = *self.state_rx.borrow(); }, From 6adaa5752168cca274a2769d77842cfdefafe4b1 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 09:47:33 +1100 Subject: [PATCH 070/232] Wip --- Cargo.lock | 1 + livekit-datatrack/Cargo.toml | 2 +- livekit-datatrack/src/local/mod.rs | 3 +- livekit-datatrack/src/remote/manager.rs | 99 ++++++++++++++++++++++-- livekit-datatrack/src/remote/mod.rs | 83 ++++++++++++++++++-- livekit-datatrack/src/remote/pipeline.rs | 55 ++++++++++--- 6 files changed, 217 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 850889170..888b346e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5628,6 +5628,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] diff --git a/livekit-datatrack/Cargo.toml b/livekit-datatrack/Cargo.toml index 00acaff79..42bb5b419 100644 --- a/livekit-datatrack/Cargo.toml +++ b/livekit-datatrack/Cargo.toml @@ -15,7 +15,7 @@ tokio = { version = "1.48.0", default-features = false, features = ["sync"] } futures-util = { version = "0.3", default-features = false, features = ["sink"] } bytes = "1.10.1" from_variants = "1.0.2" -tokio-stream = "0.1.17" +tokio-stream = { version = "0.1.17", features = ["sync"] } anyhow = "1.0.100" # For internal error handling only rand = "0.9.2" diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index e213cab46..844bd91b7 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -13,7 +13,8 @@ // limitations under the License. use crate::{ - DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, InternalError, local::manager::{LocalTrackState, UnpublishInitiator}, + local::manager::{LocalTrackState, UnpublishInitiator}, + DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, InternalError, }; use std::{fmt, marker::PhantomData, sync::Arc}; use thiserror::Error; diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 13542c09f..c9057315b 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -13,13 +13,17 @@ // limitations under the License. use crate::{ - dtp::TrackHandle, DataTrack, DataTrackInfo, DecryptionProvider, InternalError, Remote, + dtp::TrackHandle, remote::pipeline::RemoteTrackTask, DataTrack, DataTrackInfo, + DecryptionProvider, InternalError, Remote, RemoteDataTrack, RemoteTrackInner, }; use anyhow::Context; use bytes::Bytes; use from_variants::FromVariants; -use std::{collections::HashMap, sync::Arc}; -use tokio::sync::mpsc; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; +use tokio::sync::{broadcast, mpsc}; use tokio_stream::{wrappers::ReceiverStream, Stream}; /// An external event handled by [`Manager`]. @@ -70,6 +74,25 @@ pub struct SubscriptionUpdatedEvent { pub subscribe: bool, } +#[derive(Debug, Clone, Copy)] +pub(crate) enum TrackState { + Available, + Subscribed, // could include subscription details + Unpublished, +} + +impl TrackState { + pub fn is_published(&self) -> bool { + !matches!(self, Self::Unpublished) + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum TrackSubscriptionEvent { + Subscribe, // TODO: include options + Unsubscribe, +} + #[derive(Debug)] pub struct ManagerOptions { pub decryption: Option>, @@ -87,7 +110,12 @@ impl Manager { let (event_out_tx, event_out_rx) = mpsc::channel(Self::OUTPUT_BUFFER_SIZE); let manager = Manager { event_in_tx }; - let task = ManagerTask { decryption: options.decryption, event_in_rx, event_out_tx }; + let task = ManagerTask { + decryption: options.decryption, + event_in_rx, + event_out_tx, + descriptors: Default::default(), + }; let event_out_stream = ReceiverStream::new(event_out_rx); (manager, task, event_out_stream) @@ -105,10 +133,19 @@ impl Manager { const OUTPUT_BUFFER_SIZE: usize = 4; } +#[derive(Debug)] +enum Descriptor { + Available { info: DataTrackInfo, publisher_identity: String }, + Subscribed, +} + pub struct ManagerTask { decryption: Option>, event_in_rx: mpsc::Receiver, event_out_tx: mpsc::Sender, + + // Mapping between SID and descriptor. + descriptors: HashMap, } impl ManagerTask { @@ -127,7 +164,7 @@ impl ManagerTask { InputEvent::PublicationsUpdated(event) => self.handle_publications_updated(event), InputEvent::SubscriberHandles(event) => self.handle_subscriber_handles(event), InputEvent::PacketReceived(bytes) => self.handle_packet_received(bytes), - _ => Ok(()) + _ => Ok(()), } } @@ -135,7 +172,54 @@ impl ManagerTask { &mut self, event: PublicationsUpdatedEvent, ) -> Result<(), InternalError> { - todo!() + // HashMap + + let tracks_by_sid: HashMap<&str, (&str, &DataTrackInfo)> = event + .tracks_by_participant + .iter() + .map(|(participant_identity, tracks)| { + tracks.iter().map(move |track_info| { + (track_info.sid.as_str(), (participant_identity.as_str(), track_info)) + }) + }) + .flatten() + .collect(); + + let existing_sids: HashSet<_> = self.descriptors.keys().map(|key| key.as_str()).collect(); + let update_sids: HashSet<_> = tracks_by_sid.keys().map(|key| *key).collect(); + + for new_sid in update_sids.difference(&existing_sids) { + let Some((publisher_identity, info)) = tracks_by_sid.remove(new_sid) else { continue }; + self.handle_track_published(publisher_identity, info); + } + for removed_sid in existing_sids.difference(&update_sids) { + // TODO: remove descriptor, set state to invalidate object/task + } + Ok(()) + } + + fn handle_track_published(&mut self, publisher_identity: String, info: DataTrackInfo) { + let (packet_tx, packet_rx) = mpsc::channel(4); // TODO: tune + let (frame_tx, frame_rx) = broadcast::channel(4); + + let info = Arc::new(info); + let task = RemoteTrackTask { + decryption: self.decryption.clone(), + info: info.clone(), + packet_rx, + frame_tx, + }; + livekit_runtime::spawn(task.run()); + // - create & store descriptor + // include publisher identity and packet_tx + + let inner = RemoteTrackInner { frame_rx }; + let track = RemoteDataTrack::new(info, inner); + _ = self.event_out_tx.send(track.into()); + } + + fn handle_track_unpublished(&mut self, track_sid: String) { + // - end track task, invalidate object } fn handle_subscriber_handles( @@ -146,6 +230,9 @@ impl ManagerTask { } fn handle_packet_received(&mut self, bytes: Bytes) -> Result<(), InternalError> { + // Decode packet + // Lookup handle + // Forward todo!() } } diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 0d3e24fe4..6d38dd704 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -12,12 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{DataTrack, DataTrackInfo, DataTrackInner}; -use std::{marker::PhantomData, sync::Arc}; +use crate::{DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, InternalError}; +use futures_util::{StreamExt, TryFutureExt}; +use livekit_runtime::timeout; +use manager::{TrackState, TrackSubscriptionEvent}; +use std::{marker::PhantomData, sync::Arc, time::Duration}; +use thiserror::Error; +use tokio::sync::{broadcast, mpsc, watch}; +use tokio_stream::{wrappers::BroadcastStream, Stream}; mod manager; -mod proto; mod pipeline; +mod proto; /// Data track published by a remote participant. pub type RemoteDataTrack = DataTrack; @@ -39,20 +45,81 @@ impl DataTrack { } } -impl DataTrack {} +impl DataTrack { + /// Subscribe to the data track to receive frames. + pub async fn subscribe(&self) -> Result, SubscribeError> { + self.inner().subscribe().await + } + + pub fn unsubscribe_all(self) {} -#[derive(Debug, Clone)] + /// Identity of the participant who published the track. + pub fn publisher_identity(&self) -> &str { + todo!() + } + + // TODO: subscribe with options + // TODO: is_published +} + +#[derive(Debug)] pub(crate) struct RemoteTrackInner { - // frame_rx - // state... + state_rx: watch::Receiver, + subscription_tx: mpsc::Sender, + frame_rx: broadcast::Receiver, } impl RemoteTrackInner { // manage subscription + + async fn subscribe(&self) -> Result, SubscribeError> { + self.require_subscription().await?; + Ok(self.frame_stream()) + } + + async fn require_subscription(&self) -> Result<(), SubscribeError> { + match *self.state_rx.borrow() { + TrackState::Subscribed => return Ok(()), + TrackState::Unpublished => return Err(SubscribeError::Unpublished), + TrackState::Available => {} + } + self.subscription_tx + .try_send(TrackSubscriptionEvent::Subscribe) + .map_err(|e| Into::::into(e)) + .map_err(InternalError::from)?; + + let mut state_rx = self.state_rx.clone(); + let wait_for_subscribed = state_rx + .wait_for(|state| matches!(state, TrackState::Subscribed)) + .map_err(|_| SubscribeError::Disconnected); + + _ = timeout(Duration::from_secs(10), wait_for_subscribed) + .map_err(|_| SubscribeError::Timeout) + .await??; + Ok(()) + } + + fn frame_stream(&self) -> impl Stream { + // TODO: mechanism to end stream on unsubscribe but not unpublish + BroadcastStream::new(self.frame_rx.resubscribe()) + .filter_map(|result| async move { result.ok() }) + } } impl Drop for RemoteTrackInner { fn drop(&mut self) { // unsubscribe } -} \ No newline at end of file +} + +#[derive(Debug, Error)] +pub enum SubscribeError { + #[error("The track has been unpublished and is no longer available")] + Unpublished, + #[error("Request to subscribe to data track timed-out")] + Timeout, + #[error("Cannot subscribe to data track when disconnected")] + Disconnected, + #[error(transparent)] + Internal(#[from] InternalError), +} diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index ed9656876..670b04311 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -12,19 +12,54 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::InternalError; - -struct RemoteTrackTask { - // depacketizer - // decryption - // state_rx (from manager) - // frame_tx (to track inner) - // packet_in_rx - // signal_out_tx +use super::manager::{TrackState, TrackSubscriptionEvent}; +use crate::{DataTrackFrame, DataTrackInfo, DecryptionProvider, InternalError, dtp::Dtp}; +use std::sync::Arc; +use tokio::sync::{broadcast, mpsc, watch}; + +// pub enum RemoteTrackState { +// /// Track available to be subscribed to. +// Published, +// /// Local participant subscribed to the track. +// Subscribed, +// /// Track has been unpublished and is no longer available. +// Unpublished, +// } + +// update_subscription_tx +// state_rx + + + + +pub(super) struct RemoteTrackTask { + // pub depacketizer: dtp::Depacketizer, + pub decryption: Option>, + pub info: Arc, + //pub subscription_rx: mpsc::Receiver<> + pub state_rx: watch::Receiver, + pub subscription_rx: mpsc::Receiver, + pub packet_rx: mpsc::Receiver, + pub frame_tx: broadcast::Sender, + pub event_out_tx: mpsc::Sender, + // TODO: mechanism to update subscription? } impl RemoteTrackTask { - async fn run(mut self) -> Result<(), InternalError> { + pub async fn run(mut self) -> Result<(), InternalError> { + let mut state = *self.state_rx.borrow(); + while state.is_published() { + tokio::select! { + biased; + _ = self.state_rx.changed() => { + state = *self.state_rx.borrow(); + }, + // Some(frame) = self.frame_rx.recv() => { + // _ = self.publish_frame(frame).inspect_err(|err| log::error!("{}", err)); + // }, + else => break + } + } Ok(()) } } From 37bb285571e8c55b6ebc0334f60127e2c551949b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:20:03 +1100 Subject: [PATCH 071/232] Remote track implementation --- livekit-datatrack/src/common/track.rs | 2 +- livekit-datatrack/src/remote/manager.rs | 274 +++++++++++++++++------ livekit-datatrack/src/remote/mod.rs | 83 +++---- livekit-datatrack/src/remote/pipeline.rs | 56 +++-- 4 files changed, 264 insertions(+), 151 deletions(-) diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/common/track.rs index 2fa275212..ec227a030 100644 --- a/livekit-datatrack/src/common/track.rs +++ b/livekit-datatrack/src/common/track.rs @@ -20,7 +20,7 @@ use std::{marker::PhantomData, sync::Arc}; #[derive(Debug, Clone)] pub struct DataTrackInfo { pub(crate) sid: String, // TODO: use shared ID type - pub(crate) handle: TrackHandle, + pub(crate) handle: TrackHandle, // TODO: consider removing (protocol level detail) pub(crate) name: String, pub(crate) uses_e2ee: bool, } diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index c9057315b..fc663ac9a 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -13,23 +13,23 @@ // limitations under the License. use crate::{ - dtp::TrackHandle, remote::pipeline::RemoteTrackTask, DataTrack, DataTrackInfo, - DecryptionProvider, InternalError, Remote, RemoteDataTrack, RemoteTrackInner, + dtp::{Dtp, TrackHandle}, + remote::pipeline::RemoteTrackTask, + DataTrackFrame, DataTrackInfo, DecryptionProvider, InternalError, RemoteDataTrack, + RemoteTrackInner, SubscribeError, }; -use anyhow::Context; +use anyhow::{anyhow, Context}; use bytes::Bytes; use from_variants::FromVariants; -use std::{ - collections::{HashMap, HashSet}, - sync::Arc, -}; -use tokio::sync::{broadcast, mpsc}; +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::{broadcast, mpsc, oneshot, watch}; use tokio_stream::{wrappers::ReceiverStream, Stream}; /// An external event handled by [`Manager`]. -#[derive(Debug, Clone, FromVariants)] +#[derive(Debug, FromVariants)] pub enum InputEvent { PublicationsUpdated(PublicationsUpdatedEvent), + Subscribe(SubscribeEvent), SubscriberHandles(SubscriberHandlesEvent), /// Packet has been received over the transport. PacketReceived(Bytes), @@ -38,12 +38,12 @@ pub enum InputEvent { } /// An event produced by [`Manager`] requiring external action. -#[derive(Debug, Clone, FromVariants)] +#[derive(Debug, FromVariants)] pub enum OutputEvent { SubscriptionUpdated(SubscriptionUpdatedEvent), /// Remote track has been published and a track object has been created for /// the user to interact with. - TrackAvailable(DataTrack), + TrackAvailable(RemoteDataTrack), } /// Track publications updated for a specific participant. @@ -51,22 +51,32 @@ pub enum OutputEvent { /// This is used to detect newly published tracks as well as /// tracks that have been unpublished. /// -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct PublicationsUpdatedEvent { /// Mapping between participant identity and data tracks published by that participant. pub tracks_by_participant: HashMap>, } /// Subscriber handles available or updated. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct SubscriberHandlesEvent { /// Mapping between track handles attached to incoming packets to the /// track SIDs they belong to. pub mapping: HashMap, } +/// User requested to subscribe to a track. +#[derive(Debug)] +pub struct SubscribeEvent { + /// Identifier of the track. + pub(super) track_sid: String, + /// Async completion channel. + pub(super) result_tx: + oneshot::Sender, SubscribeError>>, +} + /// User subscribed or unsubscribed to a track. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct SubscriptionUpdatedEvent { /// Identifier of the affected track. pub track_sid: String, @@ -76,8 +86,7 @@ pub struct SubscriptionUpdatedEvent { #[derive(Debug, Clone, Copy)] pub(crate) enum TrackState { - Available, - Subscribed, // could include subscription details + Published, Unpublished, } @@ -87,12 +96,6 @@ impl TrackState { } } -#[derive(Debug, Clone, Copy)] -pub(crate) enum TrackSubscriptionEvent { - Subscribe, // TODO: include options - Unsubscribe, -} - #[derive(Debug)] pub struct ManagerOptions { pub decryption: Option>, @@ -109,9 +112,10 @@ impl Manager { let (event_in_tx, event_in_rx) = mpsc::channel(Self::INPUT_BUFFER_SIZE); let (event_out_tx, event_out_rx) = mpsc::channel(Self::OUTPUT_BUFFER_SIZE); - let manager = Manager { event_in_tx }; + let manager = Manager { event_in_tx: event_in_tx.clone() }; let task = ManagerTask { decryption: options.decryption, + event_in_tx: event_in_tx.downgrade(), event_in_rx, event_out_tx, descriptors: Default::default(), @@ -134,18 +138,35 @@ impl Manager { } #[derive(Debug)] -enum Descriptor { - Available { info: DataTrackInfo, publisher_identity: String }, - Subscribed, +struct Descriptor { + info: Arc, + state_tx: watch::Sender, + state: DescriptorState, +} + +#[derive(Debug)] +enum DescriptorState { + Available, + Pending { + result_txs: + Vec, SubscribeError>>>, + }, + Subscribed { + handle: TrackHandle, + packet_tx: mpsc::Sender, + frame_tx: broadcast::Sender, + }, } pub struct ManagerTask { decryption: Option>, + event_in_tx: mpsc::WeakSender, event_in_rx: mpsc::Receiver, event_out_tx: mpsc::Sender, - // Mapping between SID and descriptor. - descriptors: HashMap, + /// Mapping between SID and descriptor. + descriptors: HashMap, + // TODO: create index for fast lookup by handle } impl ManagerTask { @@ -157,6 +178,7 @@ impl ManagerTask { let Err(err) = self.handle_event(event) else { continue }; log::error!("Failed to handle input event: {}", err); } + self.shutdown(); } fn handle_event(&mut self, event: InputEvent) -> Result<(), InternalError> { @@ -164,6 +186,7 @@ impl ManagerTask { InputEvent::PublicationsUpdated(event) => self.handle_publications_updated(event), InputEvent::SubscriberHandles(event) => self.handle_subscriber_handles(event), InputEvent::PacketReceived(bytes) => self.handle_packet_received(bytes), + InputEvent::Subscribe(event) => self.handle_subscribe(event), _ => Ok(()), } } @@ -172,67 +195,174 @@ impl ManagerTask { &mut self, event: PublicationsUpdatedEvent, ) -> Result<(), InternalError> { - // HashMap - - let tracks_by_sid: HashMap<&str, (&str, &DataTrackInfo)> = event - .tracks_by_participant - .iter() - .map(|(participant_identity, tracks)| { - tracks.iter().map(move |track_info| { - (track_info.sid.as_str(), (participant_identity.as_str(), track_info)) - }) - }) - .flatten() - .collect(); - - let existing_sids: HashSet<_> = self.descriptors.keys().map(|key| key.as_str()).collect(); - let update_sids: HashSet<_> = tracks_by_sid.keys().map(|key| *key).collect(); - - for new_sid in update_sids.difference(&existing_sids) { - let Some((publisher_identity, info)) = tracks_by_sid.remove(new_sid) else { continue }; - self.handle_track_published(publisher_identity, info); - } - for removed_sid in existing_sids.difference(&update_sids) { - // TODO: remove descriptor, set state to invalidate object/task - } - Ok(()) + // TODO: diff with descriptors + // let tracks_by_sid: HashMap<&str, (&str, &DataTrackInfo)> = event + // .tracks_by_participant + // .iter() + // .map(|(participant_identity, tracks)| { + // tracks.iter().map(move |track_info| { + // (track_info.sid.as_str(), (participant_identity.as_str(), track_info)) + // }) + // }) + // .flatten() + // .collect(); + + // let existing_sids: HashSet<_> = self.descriptors.keys().map(|key| key.as_str()).collect(); + // let update_sids: HashSet<_> = tracks_by_sid.keys().map(|key| *key).collect(); + + // for new_sid in update_sids.difference(&existing_sids) { + // let Some((publisher_identity, info)) = tracks_by_sid.remove(new_sid) else { continue }; + // self.handle_track_published(publisher_identity, info); + // } + // for removed_sid in existing_sids.difference(&update_sids) { + // // TODO: remove descriptor, set state to invalidate object/task + // } + // Ok(()) + todo!() } fn handle_track_published(&mut self, publisher_identity: String, info: DataTrackInfo) { - let (packet_tx, packet_rx) = mpsc::channel(4); // TODO: tune - let (frame_tx, frame_rx) = broadcast::channel(4); - + if self.descriptors.contains_key(&info.sid) { + log::error!("Existing descriptor for track {}", info.sid); + return; + } let info = Arc::new(info); - let task = RemoteTrackTask { - decryption: self.decryption.clone(), - info: info.clone(), - packet_rx, - frame_tx, - }; - livekit_runtime::spawn(task.run()); - // - create & store descriptor - // include publisher identity and packet_tx - let inner = RemoteTrackInner { frame_rx }; + let (state_tx, state_rx) = watch::channel(TrackState::Published); + let descriptor = + Descriptor { info: info.clone(), state_tx, state: DescriptorState::Available }; + self.descriptors.insert(descriptor.info.sid.clone(), descriptor); + + let inner = RemoteTrackInner { + state_rx, + event_in_tx: self.event_in_tx.clone(), + publisher_identity, + }; let track = RemoteDataTrack::new(info, inner); - _ = self.event_out_tx.send(track.into()); + self.event_out_tx.send(track.into()); } fn handle_track_unpublished(&mut self, track_sid: String) { - // - end track task, invalidate object + let Some(descriptor) = self.descriptors.remove(&track_sid) else { + log::error!("Unknown track {}", track_sid); + return; + }; + descriptor.state_tx.send(TrackState::Unpublished); + // TODO: this should end the track task + } + + fn handle_subscribe(&mut self, event: SubscribeEvent) -> Result<(), InternalError> { + let Some(descriptor) = self.descriptors.get_mut(event.track_sid.as_str()) else { + _ = event.result_tx.send(Err(SubscribeError::Internal( + anyhow!("Cannot subscribe to unknown track").into(), + ))); + return Ok(()); + }; + match &mut descriptor.state { + DescriptorState::Available => { + let update_event = SubscriptionUpdatedEvent { + track_sid: event.track_sid.to_string(), + subscribe: true, + }; + _ = self.event_out_tx.send(update_event.into()); + descriptor.state = DescriptorState::Pending { result_txs: vec![event.result_tx] }; + // TODO: schedule timeout internally + } + DescriptorState::Pending { result_txs } => { + result_txs.push(event.result_tx); + } + DescriptorState::Subscribed { handle: _, packet_tx: _, frame_tx } => { + let frame_rx = frame_tx.subscribe(); + _ = event.result_tx.send(Ok(frame_rx)) + } + } + Ok(()) } fn handle_subscriber_handles( &mut self, event: SubscriberHandlesEvent, ) -> Result<(), InternalError> { - todo!() + for (handle, sid) in event.mapping { + self.register_subscriber_handle(handle, sid); + } + Ok(()) + } + + fn register_subscriber_handle(&mut self, handle: TrackHandle, sid: String) { + let Some(descriptor) = self.descriptors.get_mut(&sid) else { + log::warn!("Unknown track: {}", sid); + return; + }; + match &mut descriptor.state { + DescriptorState::Available => log::warn!("No subscription"), + DescriptorState::Pending { result_txs } => { + let (packet_tx, packet_rx) = mpsc::channel(4); // TODO: tune + let (frame_tx, frame_rx) = broadcast::channel(4); + + let track_task = RemoteTrackTask { + decryption: self.decryption.clone(), + info: descriptor.info.clone(), + state_rx: descriptor.state_tx.subscribe(), + packet_rx, + frame_tx: frame_tx.clone(), + event_out_tx: self.event_out_tx.downgrade(), + }; + // TODO: spawn task + + descriptor.state = DescriptorState::Subscribed { handle, packet_tx, frame_tx }; + // TODO: send completion + // for result_tx in result_txs { + // result_tx.send(Ok(frame_rx.resubscribe())); + // } + } + DescriptorState::Subscribed { handle: existing_handle, packet_tx: _, frame_tx: _ } => { + *existing_handle = handle + } + } } fn handle_packet_received(&mut self, bytes: Bytes) -> Result<(), InternalError> { - // Decode packet - // Lookup handle - // Forward - todo!() + let dtp = Dtp::deserialize(bytes).context("Failed to deserialize packet")?; + + // TODO: this is O(n), use index instead + let descriptor = self + .descriptors + .iter() + .find(|(_, descriptor)| match descriptor.state { + DescriptorState::Subscribed { handle, packet_tx: _, frame_tx: _ } => { + dtp.header.track_handle == handle + } + _ => false, + }) + .map(|entry| entry.1); + + let Some(descriptor) = descriptor else { + Err(anyhow!("Received packet for unknown track {}", dtp.header.track_handle))? + }; + let DescriptorState::Subscribed { handle: _, packet_tx, frame_tx: _ } = &descriptor.state + else { + Err(anyhow!( + "Received packet for track {} without subscription", + dtp.header.track_handle + ))? + }; + packet_tx.send(dtp); + Ok(()) + } + + /// Performs cleanup before the task ends. + fn shutdown(self) { + for (_, descriptor) in self.descriptors { + match descriptor.state { + DescriptorState::Available | DescriptorState::Subscribed { .. } => {} + DescriptorState::Pending { result_txs } => { + for result_tx in result_txs { + result_tx.send(Err(SubscribeError::Disconnected)); + } + } + } + descriptor.state_tx.send(TrackState::Unpublished); + } } } diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 6d38dd704..83c51c5e8 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -13,12 +13,13 @@ // limitations under the License. use crate::{DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, InternalError}; -use futures_util::{StreamExt, TryFutureExt}; +use anyhow::anyhow; +use futures_util::StreamExt; use livekit_runtime::timeout; -use manager::{TrackState, TrackSubscriptionEvent}; +use manager::{SubscribeEvent, TrackState}; use std::{marker::PhantomData, sync::Arc, time::Duration}; use thiserror::Error; -use tokio::sync::{broadcast, mpsc, watch}; +use tokio::sync::{mpsc, oneshot, watch}; use tokio_stream::{wrappers::BroadcastStream, Stream}; mod manager; @@ -48,68 +49,42 @@ impl DataTrack { impl DataTrack { /// Subscribe to the data track to receive frames. pub async fn subscribe(&self) -> Result, SubscribeError> { - self.inner().subscribe().await + let (result_tx, result_rx) = oneshot::channel(); + let subscribe_event = SubscribeEvent { track_sid: self.info.sid.clone(), result_tx }; + self.inner() + .event_in_tx + .upgrade() + .ok_or(SubscribeError::Disconnected)? + .send_timeout(subscribe_event.into(), Duration::from_millis(50)) + .await + .map_err(|_| { + SubscribeError::Internal(anyhow!("Failed to send subscribe event").into()) + })?; + + // TODO: standardize timeout + let frame_rx = timeout(Duration::from_secs(10), result_rx) + .await + .map_err(|_| SubscribeError::Timeout)? + .map_err(|_| SubscribeError::Disconnected)??; + + let frame_stream = + BroadcastStream::new(frame_rx).filter_map(|result| async move { result.ok() }); + Ok(frame_stream) } - pub fn unsubscribe_all(self) {} - /// Identity of the participant who published the track. pub fn publisher_identity(&self) -> &str { - todo!() + &self.inner().publisher_identity } - // TODO: subscribe with options // TODO: is_published } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct RemoteTrackInner { + publisher_identity: String, state_rx: watch::Receiver, - subscription_tx: mpsc::Sender, - frame_rx: broadcast::Receiver, -} - -impl RemoteTrackInner { - // manage subscription - - async fn subscribe(&self) -> Result, SubscribeError> { - self.require_subscription().await?; - Ok(self.frame_stream()) - } - - async fn require_subscription(&self) -> Result<(), SubscribeError> { - match *self.state_rx.borrow() { - TrackState::Subscribed => return Ok(()), - TrackState::Unpublished => return Err(SubscribeError::Unpublished), - TrackState::Available => {} - } - self.subscription_tx - .try_send(TrackSubscriptionEvent::Subscribe) - .map_err(|e| Into::::into(e)) - .map_err(InternalError::from)?; - - let mut state_rx = self.state_rx.clone(); - let wait_for_subscribed = state_rx - .wait_for(|state| matches!(state, TrackState::Subscribed)) - .map_err(|_| SubscribeError::Disconnected); - - _ = timeout(Duration::from_secs(10), wait_for_subscribed) - .map_err(|_| SubscribeError::Timeout) - .await??; - Ok(()) - } - - fn frame_stream(&self) -> impl Stream { - // TODO: mechanism to end stream on unsubscribe but not unpublish - BroadcastStream::new(self.frame_rx.resubscribe()) - .filter_map(|result| async move { result.ok() }) - } -} - -impl Drop for RemoteTrackInner { - fn drop(&mut self) { - // unsubscribe - } + event_in_tx: mpsc::WeakSender, } #[derive(Debug, Error)] diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 670b04311..e2e1e7a42 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -12,37 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::manager::{TrackState, TrackSubscriptionEvent}; -use crate::{DataTrackFrame, DataTrackInfo, DecryptionProvider, InternalError, dtp::Dtp}; +use super::manager::{TrackState}; +use crate::{DataTrackFrame, DataTrackInfo, DecryptionProvider, EncryptedPayload, InternalError, dtp::Dtp}; use std::sync::Arc; use tokio::sync::{broadcast, mpsc, watch}; -// pub enum RemoteTrackState { -// /// Track available to be subscribed to. -// Published, -// /// Local participant subscribed to the track. -// Subscribed, -// /// Track has been unpublished and is no longer available. -// Unpublished, -// } - -// update_subscription_tx -// state_rx - - - - pub(super) struct RemoteTrackTask { // pub depacketizer: dtp::Depacketizer, pub decryption: Option>, pub info: Arc, - //pub subscription_rx: mpsc::Receiver<> pub state_rx: watch::Receiver, - pub subscription_rx: mpsc::Receiver, pub packet_rx: mpsc::Receiver, pub frame_tx: broadcast::Sender, - pub event_out_tx: mpsc::Sender, - // TODO: mechanism to update subscription? + pub event_out_tx: mpsc::WeakSender, } impl RemoteTrackTask { @@ -54,12 +36,38 @@ impl RemoteTrackTask { _ = self.state_rx.changed() => { state = *self.state_rx.borrow(); }, - // Some(frame) = self.frame_rx.recv() => { - // _ = self.publish_frame(frame).inspect_err(|err| log::error!("{}", err)); - // }, + Some(dtp) = self.packet_rx.recv() => { + self.receive_packet(dtp); + }, else => break } } + // TODO: send unsubscribe if needed Ok(()) } + + async fn receive_packet(&mut self, mut dtp: Dtp) { + if let Some(decryption) = &self.decryption { + debug_assert!(self.info.uses_e2ee); + + let Some(e2ee_meta) = dtp.header.e2ee else { + log::error!("Missing E2EE meta"); + return; + }; + let encrypted_payload = EncryptedPayload { + payload: dtp.payload, + iv: e2ee_meta.iv, + key_index: e2ee_meta.key_index, + }; + let decrypted_payload = match decryption.decrypt(encrypted_payload) { + Ok(decrypted_payload) => decrypted_payload, + Err(err) => { + log::error!("Decryption failed: {}", err); + return + } + }; + dtp.payload = decrypted_payload; + } + // TODO: depacketize, emit complete frame + } } From f3c12512dedc3b4c20cb2e7da3eabb55a8391455 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:41:40 +1100 Subject: [PATCH 072/232] Cleaner error handling --- livekit-datatrack/src/remote/manager.rs | 72 +++++++++++-------------- 1 file changed, 30 insertions(+), 42 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index fc663ac9a..23f717a38 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -172,29 +172,18 @@ pub struct ManagerTask { impl ManagerTask { pub async fn run(mut self) { while let Some(event) = self.event_in_rx.recv().await { - if matches!(event, InputEvent::Shutdown) { - break; + match event { + InputEvent::PublicationsUpdated(event) => self.handle_publications_updated(event), + InputEvent::Subscribe(event) => self.handle_subscribe(event), + InputEvent::SubscriberHandles(event) => self.handle_subscriber_handles(event), + InputEvent::PacketReceived(bytes) => self.handle_packet_received(bytes), + InputEvent::Shutdown => break, } - let Err(err) = self.handle_event(event) else { continue }; - log::error!("Failed to handle input event: {}", err); } self.shutdown(); } - fn handle_event(&mut self, event: InputEvent) -> Result<(), InternalError> { - match event { - InputEvent::PublicationsUpdated(event) => self.handle_publications_updated(event), - InputEvent::SubscriberHandles(event) => self.handle_subscriber_handles(event), - InputEvent::PacketReceived(bytes) => self.handle_packet_received(bytes), - InputEvent::Subscribe(event) => self.handle_subscribe(event), - _ => Ok(()), - } - } - - fn handle_publications_updated( - &mut self, - event: PublicationsUpdatedEvent, - ) -> Result<(), InternalError> { + fn handle_publications_updated(&mut self, event: PublicationsUpdatedEvent) { // TODO: diff with descriptors // let tracks_by_sid: HashMap<&str, (&str, &DataTrackInfo)> = event // .tracks_by_participant @@ -239,7 +228,7 @@ impl ManagerTask { publisher_identity, }; let track = RemoteDataTrack::new(info, inner); - self.event_out_tx.send(track.into()); + _ = self.event_out_tx.send(track.into()); } fn handle_track_unpublished(&mut self, track_sid: String) { @@ -247,16 +236,16 @@ impl ManagerTask { log::error!("Unknown track {}", track_sid); return; }; - descriptor.state_tx.send(TrackState::Unpublished); + _ = descriptor.state_tx.send(TrackState::Unpublished); // TODO: this should end the track task } - fn handle_subscribe(&mut self, event: SubscribeEvent) -> Result<(), InternalError> { + fn handle_subscribe(&mut self, event: SubscribeEvent) { let Some(descriptor) = self.descriptors.get_mut(event.track_sid.as_str()) else { - _ = event.result_tx.send(Err(SubscribeError::Internal( - anyhow!("Cannot subscribe to unknown track").into(), - ))); - return Ok(()); + let error = + SubscribeError::Internal(anyhow!("Cannot subscribe to unknown track").into()); + _ = event.result_tx.send(Err(error)); + return; }; match &mut descriptor.state { DescriptorState::Available => { @@ -276,17 +265,12 @@ impl ManagerTask { _ = event.result_tx.send(Ok(frame_rx)) } } - Ok(()) } - fn handle_subscriber_handles( - &mut self, - event: SubscriberHandlesEvent, - ) -> Result<(), InternalError> { + fn handle_subscriber_handles(&mut self, event: SubscriberHandlesEvent) { for (handle, sid) in event.mapping { self.register_subscriber_handle(handle, sid); } - Ok(()) } fn register_subscriber_handle(&mut self, handle: TrackHandle, sid: String) { @@ -322,8 +306,14 @@ impl ManagerTask { } } - fn handle_packet_received(&mut self, bytes: Bytes) -> Result<(), InternalError> { - let dtp = Dtp::deserialize(bytes).context("Failed to deserialize packet")?; + fn handle_packet_received(&mut self, bytes: Bytes) { + let dtp = match Dtp::deserialize(bytes) { + Ok(dtp) => dtp, + Err(err) => { + log::error!("Failed to deserialize DTP: {}", err); + return; + } + }; // TODO: this is O(n), use index instead let descriptor = self @@ -338,17 +328,15 @@ impl ManagerTask { .map(|entry| entry.1); let Some(descriptor) = descriptor else { - Err(anyhow!("Received packet for unknown track {}", dtp.header.track_handle))? + log::warn!("Received packet for unknown track {}", dtp.header.track_handle); + return; }; let DescriptorState::Subscribed { handle: _, packet_tx, frame_tx: _ } = &descriptor.state else { - Err(anyhow!( - "Received packet for track {} without subscription", - dtp.header.track_handle - ))? + log::warn!("Received packet for track {} without subscription", descriptor.info.sid); + return; }; - packet_tx.send(dtp); - Ok(()) + _ = packet_tx.send(dtp); } /// Performs cleanup before the task ends. @@ -358,11 +346,11 @@ impl ManagerTask { DescriptorState::Available | DescriptorState::Subscribed { .. } => {} DescriptorState::Pending { result_txs } => { for result_tx in result_txs { - result_tx.send(Err(SubscribeError::Disconnected)); + _ = result_tx.send(Err(SubscribeError::Disconnected)); } } } - descriptor.state_tx.send(TrackState::Unpublished); + _ = descriptor.state_tx.send(TrackState::Unpublished); } } } From 50ca3daeb76302d18940fd7ff6e728fa8edca326 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:57:23 +1100 Subject: [PATCH 073/232] Detect publication changes --- livekit-datatrack/src/remote/manager.rs | 48 ++++++++++++------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 23f717a38..6aa9e6ecd 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -21,7 +21,10 @@ use crate::{ use anyhow::{anyhow, Context}; use bytes::Bytes; use from_variants::FromVariants; -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use tokio::sync::{broadcast, mpsc, oneshot, watch}; use tokio_stream::{wrappers::ReceiverStream, Stream}; @@ -184,30 +187,25 @@ impl ManagerTask { } fn handle_publications_updated(&mut self, event: PublicationsUpdatedEvent) { - // TODO: diff with descriptors - // let tracks_by_sid: HashMap<&str, (&str, &DataTrackInfo)> = event - // .tracks_by_participant - // .iter() - // .map(|(participant_identity, tracks)| { - // tracks.iter().map(move |track_info| { - // (track_info.sid.as_str(), (participant_identity.as_str(), track_info)) - // }) - // }) - // .flatten() - // .collect(); - - // let existing_sids: HashSet<_> = self.descriptors.keys().map(|key| key.as_str()).collect(); - // let update_sids: HashSet<_> = tracks_by_sid.keys().map(|key| *key).collect(); - - // for new_sid in update_sids.difference(&existing_sids) { - // let Some((publisher_identity, info)) = tracks_by_sid.remove(new_sid) else { continue }; - // self.handle_track_published(publisher_identity, info); - // } - // for removed_sid in existing_sids.difference(&update_sids) { - // // TODO: remove descriptor, set state to invalidate object/task - // } - // Ok(()) - todo!() + let mut sids_in_update = HashSet::new(); + + // Detect published track + for (publisher_identity, tracks) in event.tracks_by_participant { + for info in tracks { + sids_in_update.insert(info.sid.clone()); + if self.descriptors.contains_key(&info.sid) { + continue; + } + self.handle_track_published(publisher_identity.clone(), info); + } + } + + // Detect unpublished tracks + let unpublished_sids: Vec<_> = + self.descriptors.keys().filter(|sid| !sids_in_update.contains(*sid)).cloned().collect(); + for sid in unpublished_sids { + self.handle_track_unpublished(sid.clone()); + } } fn handle_track_published(&mut self, publisher_identity: String, info: DataTrackInfo) { From ad779b15d6b861effd0dd85ebf9e4c43b9fc9fd4 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 16:15:31 +1100 Subject: [PATCH 074/232] Efficient handle mapping --- livekit-datatrack/src/remote/manager.rs | 47 +++++++++----------- livekit-datatrack/src/utils/handle_map.rs | 53 +++++++++++++++++++++++ livekit-datatrack/src/utils/mod.rs | 4 ++ 3 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 livekit-datatrack/src/utils/handle_map.rs diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 6aa9e6ecd..a13381655 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -15,6 +15,7 @@ use crate::{ dtp::{Dtp, TrackHandle}, remote::pipeline::RemoteTrackTask, + utils::HandleMap, DataTrackFrame, DataTrackInfo, DecryptionProvider, InternalError, RemoteDataTrack, RemoteTrackInner, SubscribeError, }; @@ -121,7 +122,8 @@ impl Manager { event_in_tx: event_in_tx.downgrade(), event_in_rx, event_out_tx, - descriptors: Default::default(), + descriptors: HashMap::default(), + sub_handles: HandleMap::default(), }; let event_out_stream = ReceiverStream::new(event_out_rx); @@ -155,7 +157,6 @@ enum DescriptorState { Vec, SubscribeError>>>, }, Subscribed { - handle: TrackHandle, packet_tx: mpsc::Sender, frame_tx: broadcast::Sender, }, @@ -167,9 +168,10 @@ pub struct ManagerTask { event_in_rx: mpsc::Receiver, event_out_tx: mpsc::Sender, - /// Mapping between SID and descriptor. + /// Mapping between track SID and descriptor. descriptors: HashMap, - // TODO: create index for fast lookup by handle + /// Bidirectional mapping between track SID and subscriber handle. + sub_handles: HandleMap, } impl ManagerTask { @@ -230,11 +232,12 @@ impl ManagerTask { } fn handle_track_unpublished(&mut self, track_sid: String) { + self.sub_handles.remove(&track_sid); let Some(descriptor) = self.descriptors.remove(&track_sid) else { log::error!("Unknown track {}", track_sid); return; }; - _ = descriptor.state_tx.send(TrackState::Unpublished); + _ = descriptor.state_tx.send(TrackState::Unpublished); // TODO: this should end the track task } @@ -258,7 +261,7 @@ impl ManagerTask { DescriptorState::Pending { result_txs } => { result_txs.push(event.result_tx); } - DescriptorState::Subscribed { handle: _, packet_tx: _, frame_tx } => { + DescriptorState::Subscribed { packet_tx: _, frame_tx } => { let frame_rx = frame_tx.subscribe(); _ = event.result_tx.send(Ok(frame_rx)) } @@ -292,14 +295,16 @@ impl ManagerTask { }; // TODO: spawn task - descriptor.state = DescriptorState::Subscribed { handle, packet_tx, frame_tx }; + descriptor.state = DescriptorState::Subscribed { packet_tx, frame_tx }; + self.sub_handles.insert(handle, sid); + // TODO: send completion // for result_tx in result_txs { // result_tx.send(Ok(frame_rx.resubscribe())); // } } - DescriptorState::Subscribed { handle: existing_handle, packet_tx: _, frame_tx: _ } => { - *existing_handle = handle + DescriptorState::Subscribed { packet_tx: _, frame_tx: _ } => { + log::warn!("Handle reassignment not implemented"); } } } @@ -312,25 +317,15 @@ impl ManagerTask { return; } }; - - // TODO: this is O(n), use index instead - let descriptor = self - .descriptors - .iter() - .find(|(_, descriptor)| match descriptor.state { - DescriptorState::Subscribed { handle, packet_tx: _, frame_tx: _ } => { - dtp.header.track_handle == handle - } - _ => false, - }) - .map(|entry| entry.1); - - let Some(descriptor) = descriptor else { - log::warn!("Received packet for unknown track {}", dtp.header.track_handle); + let Some(sid) = self.sub_handles.get_sid(dtp.header.track_handle) else { + log::warn!("Unknown subscriber handle {}", dtp.header.track_handle); + return; + }; + let Some(descriptor) = self.descriptors.get(sid) else { + log::warn!("Missing descriptor"); return; }; - let DescriptorState::Subscribed { handle: _, packet_tx, frame_tx: _ } = &descriptor.state - else { + let DescriptorState::Subscribed { packet_tx, frame_tx: _ } = &descriptor.state else { log::warn!("Received packet for track {} without subscription", descriptor.info.sid); return; }; diff --git a/livekit-datatrack/src/utils/handle_map.rs b/livekit-datatrack/src/utils/handle_map.rs new file mode 100644 index 000000000..8dfbd6467 --- /dev/null +++ b/livekit-datatrack/src/utils/handle_map.rs @@ -0,0 +1,53 @@ +// Copyright 2025 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 crate::dtp::TrackHandle; +use std::collections::HashMap; + +/// Map between track handle and SID. +/// +/// All operations are O(1). +/// +#[derive(Debug, Default)] +pub struct HandleMap { + sid_to_handle: HashMap, + handle_to_sid: HashMap, +} + +impl HandleMap { + /// Insert the given mapping between track handle and SID. + /// + /// Returns a Boolean indicating whether the entry was inserted. + /// Insertion will fail if the mapping already exists in either direction. + /// + pub fn insert(&mut self, handle: TrackHandle, sid: String) -> bool { + if self.sid_to_handle.contains_key(&sid) || self.handle_to_sid.contains_key(&handle) { + return false; + } + self.sid_to_handle.insert(sid.clone(), handle); + self.handle_to_sid.insert(handle, sid); + return true; + } + + /// Get the SID associated with the given handle. + pub fn get_sid(&self, handle: TrackHandle) -> Option<&String> { + self.handle_to_sid.get(&handle) + } + + /// Remove the mapping with the given SID. + pub fn remove(&mut self, sid: &str) { + let Some(handle) = self.sid_to_handle.remove(sid) else { return }; + self.handle_to_sid.remove(&handle); + } +} diff --git a/livekit-datatrack/src/utils/mod.rs b/livekit-datatrack/src/utils/mod.rs index de7957d22..bbaa84889 100644 --- a/livekit-datatrack/src/utils/mod.rs +++ b/livekit-datatrack/src/utils/mod.rs @@ -15,7 +15,11 @@ /// Utilities for working with [`Bytes::bytes`]. mod bytes; +/// Map between track handle and SID. +mod handle_map; + mod counter; pub use bytes::*; pub use counter::*; +pub use handle_map::*; From 527bb03968b14e0389ef7ff915febe02afce2c9a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 16:18:06 +1100 Subject: [PATCH 075/232] Remove result --- livekit-datatrack/src/remote/pipeline.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index e2e1e7a42..2fea5ce00 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::manager::{TrackState}; -use crate::{DataTrackFrame, DataTrackInfo, DecryptionProvider, EncryptedPayload, InternalError, dtp::Dtp}; +use super::manager::TrackState; +use crate::{dtp::Dtp, DataTrackFrame, DataTrackInfo, DecryptionProvider, EncryptedPayload}; use std::sync::Arc; use tokio::sync::{broadcast, mpsc, watch}; @@ -28,7 +28,7 @@ pub(super) struct RemoteTrackTask { } impl RemoteTrackTask { - pub async fn run(mut self) -> Result<(), InternalError> { + pub async fn run(mut self) { let mut state = *self.state_rx.borrow(); while state.is_published() { tokio::select! { @@ -43,7 +43,6 @@ impl RemoteTrackTask { } } // TODO: send unsubscribe if needed - Ok(()) } async fn receive_packet(&mut self, mut dtp: Dtp) { @@ -63,7 +62,7 @@ impl RemoteTrackTask { Ok(decrypted_payload) => decrypted_payload, Err(err) => { log::error!("Decryption failed: {}", err); - return + return; } }; dtp.payload = decrypted_payload; From 58312a5cd50d68551c6d7f2536bb2fe1eb246475 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 16:22:13 +1100 Subject: [PATCH 076/232] Is published method --- livekit-datatrack/src/remote/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 83c51c5e8..980ff4c7a 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -72,12 +72,19 @@ impl DataTrack { Ok(frame_stream) } + /// Whether or not the track is still published. + /// + /// Once the track has been unpublished, calls to [`Self::subscribe()`] will + /// result in an error. + /// + pub fn is_published(&self) -> bool { + self.inner().state_rx.borrow().is_published() + } + /// Identity of the participant who published the track. pub fn publisher_identity(&self) -> &str { &self.inner().publisher_identity } - - // TODO: is_published } #[derive(Debug, Clone)] From 306a0a206661154eb912633f9ae94959773df7d6 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 17:13:08 +1100 Subject: [PATCH 077/232] Group public exports into API/internal --- livekit-datatrack/src/{common => }/e2ee.rs | 0 .../src/{common/mod.rs => error.rs} | 6 ---- livekit-datatrack/src/{common => }/frame.rs | 0 livekit-datatrack/src/lib.rs | 35 +++++++++++++++++-- livekit-datatrack/src/local/manager.rs | 6 ++-- livekit-datatrack/src/local/mod.rs | 10 +++--- livekit-datatrack/src/local/pipeline.rs | 3 +- livekit-datatrack/src/local/proto.rs | 4 +-- livekit-datatrack/src/remote/manager.rs | 5 +-- livekit-datatrack/src/remote/mod.rs | 7 ++-- livekit-datatrack/src/remote/pipeline.rs | 6 +++- livekit-datatrack/src/remote/proto.rs | 5 ++- livekit-datatrack/src/{common => }/track.rs | 0 livekit/src/room/data_track.rs | 7 ++-- 14 files changed, 66 insertions(+), 28 deletions(-) rename livekit-datatrack/src/{common => }/e2ee.rs (100%) rename livekit-datatrack/src/{common/mod.rs => error.rs} (90%) rename livekit-datatrack/src/{common => }/frame.rs (100%) rename livekit-datatrack/src/{common => }/track.rs (100%) diff --git a/livekit-datatrack/src/common/e2ee.rs b/livekit-datatrack/src/e2ee.rs similarity index 100% rename from livekit-datatrack/src/common/e2ee.rs rename to livekit-datatrack/src/e2ee.rs diff --git a/livekit-datatrack/src/common/mod.rs b/livekit-datatrack/src/error.rs similarity index 90% rename from livekit-datatrack/src/common/mod.rs rename to livekit-datatrack/src/error.rs index ffd2ac026..b6e4a36e0 100644 --- a/livekit-datatrack/src/common/mod.rs +++ b/livekit-datatrack/src/error.rs @@ -14,12 +14,6 @@ use thiserror::Error; -mod e2ee; -mod frame; -mod track; - -pub use {e2ee::*, frame::*, track::*}; - #[derive(Debug, Error)] #[error(transparent)] pub struct InternalError(#[from] anyhow::Error); diff --git a/livekit-datatrack/src/common/frame.rs b/livekit-datatrack/src/frame.rs similarity index 100% rename from livekit-datatrack/src/common/frame.rs rename to livekit-datatrack/src/frame.rs diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index ecb10c60d..7b3b9fddd 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -12,11 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod common; +/// Common types for local and remote tracks. +mod track; + +/// Local track publication. mod local; + +/// Remote track subscription. mod remote; +/// Frame and frame builder. +mod frame; + +/// End-to-end encryption. +mod e2ee; + +/// Data track packet serialization & packetization. mod dtp; + +/// Common utilities. mod utils; -pub use {common::*, local::*, remote::*}; +/// Internal error. +mod error; + +/// Public APIs to be re-exported by the LiveKit crate. +pub mod api { + pub use crate::{error::*, frame::*, local::*, remote::*, track::*}; +} + +/// Internal APIs for use within the LiveKit crate. +pub mod internal { + pub use crate::e2ee::*; + pub mod local { + pub use crate::local::{manager::*, proto::*}; + } + pub mod remote { + pub use crate::remote::{manager::*, proto::*}; + } +} \ No newline at end of file diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 4bc91bdd0..dd121cf29 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -14,9 +14,11 @@ use super::{pipeline::LocalTrackTask, LocalTrackInner}; use crate::{ - dtp, DataTrackInfo, DataTrackOptions, EncryptionProvider, InternalError, PublishError, + api::{DataTrackInfo, DataTrackOptions, InternalError, PublishError}, + dtp::{self, TrackHandle}, + e2ee::EncryptionProvider, + local::LocalDataTrack, }; -use crate::{dtp::TrackHandle, LocalDataTrack}; use anyhow::{anyhow, Context}; use bytes::Bytes; use from_variants::FromVariants; diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 844bd91b7..c75ccb72d 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -13,16 +13,18 @@ // limitations under the License. use crate::{ - local::manager::{LocalTrackState, UnpublishInitiator}, - DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, InternalError, + api::{DataTrack, DataTrackFrame, DataTrackInfo, InternalError}, + track::DataTrackInner, }; +use manager::{LocalTrackState, UnpublishInitiator}; use std::{fmt, marker::PhantomData, sync::Arc}; use thiserror::Error; use tokio::sync::{mpsc, watch}; -mod manager; +pub(crate) mod manager; +pub(crate) mod proto; + mod pipeline; -mod proto; /// Data track published by the local participant. pub type LocalDataTrack = DataTrack; diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index abbf40cce..e3eb9026a 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -13,9 +13,10 @@ // limitations under the License. use crate::{ + api::{DataTrackFrame, DataTrackInfo, InternalError}, dtp, + e2ee::EncryptionProvider, local::manager::{LocalTrackState, OutputEvent, UnpublishInitiator, UnpublishRequestEvent}, - DataTrackFrame, DataTrackInfo, EncryptionProvider, InternalError, }; use anyhow::Context; use std::sync::Arc; diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index 8a50b7dde..7bd0e9afc 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -14,8 +14,8 @@ use super::manager::{PublishRequestEvent, UnpublishRequestEvent}; use crate::{ - dtp::TrackHandle, local::manager::PublishResultEvent, DataTrackInfo, InternalError, - PublishError, + dtp::TrackHandle, local::manager::PublishResultEvent, + api::{DataTrackInfo, InternalError, PublishError} }; use anyhow::{anyhow, Context}; use livekit_protocol as proto; diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index a13381655..9a0c3c819 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -12,12 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::{RemoteDataTrack, RemoteTrackInner}; use crate::{ + api::{DataTrackFrame, DataTrackInfo, InternalError, SubscribeError}, dtp::{Dtp, TrackHandle}, + e2ee::DecryptionProvider, remote::pipeline::RemoteTrackTask, utils::HandleMap, - DataTrackFrame, DataTrackInfo, DecryptionProvider, InternalError, RemoteDataTrack, - RemoteTrackInner, SubscribeError, }; use anyhow::{anyhow, Context}; use bytes::Bytes; diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 980ff4c7a..08a4cffef 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, InternalError}; +use crate::api::{DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, InternalError}; use anyhow::anyhow; use futures_util::StreamExt; use livekit_runtime::timeout; @@ -22,9 +22,10 @@ use thiserror::Error; use tokio::sync::{mpsc, oneshot, watch}; use tokio_stream::{wrappers::BroadcastStream, Stream}; -mod manager; +pub(crate) mod manager; +pub(crate) mod proto; + mod pipeline; -mod proto; /// Data track published by a remote participant. pub type RemoteDataTrack = DataTrack; diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 2fea5ce00..7c26c8582 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -13,7 +13,11 @@ // limitations under the License. use super::manager::TrackState; -use crate::{dtp::Dtp, DataTrackFrame, DataTrackInfo, DecryptionProvider, EncryptedPayload}; +use crate::{ + api::{DataTrackFrame, DataTrackInfo}, + e2ee::{DecryptionProvider, EncryptedPayload}, + dtp::Dtp, +}; use std::sync::Arc; use tokio::sync::{broadcast, mpsc, watch}; diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index f569c0261..44ce04ac9 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -13,7 +13,10 @@ // limitations under the License. use super::manager::{PublicationsUpdatedEvent, SubscriberHandlesEvent, SubscriptionUpdatedEvent}; -use crate::{dtp::TrackHandle, DataTrackInfo, InternalError}; +use crate::{ + api::{DataTrackInfo, InternalError}, + dtp::TrackHandle, +}; use livekit_protocol::{self as proto, ParticipantInfo}; use std::{collections::HashMap, mem}; diff --git a/livekit-datatrack/src/common/track.rs b/livekit-datatrack/src/track.rs similarity index 100% rename from livekit-datatrack/src/common/track.rs rename to livekit-datatrack/src/track.rs diff --git a/livekit/src/room/data_track.rs b/livekit/src/room/data_track.rs index 3eb413eac..4f8e0bfd9 100644 --- a/livekit/src/room/data_track.rs +++ b/livekit/src/room/data_track.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Re-export data track API types. - -pub use livekit_datatrack::{error::*, frame::*, track::*}; -pub(crate) use livekit_datatrack::manager::*; +// Re-export api types publicly, scope internal types to crate. +pub use livekit_datatrack::api::*; +pub(crate) use livekit_datatrack::internal; \ No newline at end of file From 8e2094c210e98b0b33c6f2ef7875fbbb427b140c Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 17:18:37 +1100 Subject: [PATCH 078/232] E2ee provider implementations --- livekit/src/room/e2ee/data_track.rs | 30 +++++++++++++++++++++++++++++ livekit/src/room/e2ee/manager.rs | 21 +------------------- livekit/src/room/e2ee/mod.rs | 3 +++ 3 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 livekit/src/room/e2ee/data_track.rs diff --git a/livekit/src/room/e2ee/data_track.rs b/livekit/src/room/e2ee/data_track.rs new file mode 100644 index 000000000..c653a1117 --- /dev/null +++ b/livekit/src/room/e2ee/data_track.rs @@ -0,0 +1,30 @@ +// Copyright 2025 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 crate::{ + data_track::internal::{DecryptionProvider, E2eeError, EncryptedPayload, EncryptionProvider}, + E2eeManager, +}; + +impl DecryptionProvider for E2eeManager { + fn decrypt(&self, payload: EncryptedPayload) -> Result { + todo!() + } +} + +impl EncryptionProvider for E2eeManager { + fn encrypt(&self, payload: bytes::Bytes) -> Result { + todo!() + } +} diff --git a/livekit/src/room/e2ee/manager.rs b/livekit/src/room/e2ee/manager.rs index 2057a4453..dc18e699b 100644 --- a/livekit/src/room/e2ee/manager.rs +++ b/livekit/src/room/e2ee/manager.rs @@ -25,7 +25,6 @@ use parking_lot::Mutex; use super::{key_provider::KeyProvider, EncryptionType}; use crate::{ - data_track, e2ee::E2eeOptions, id::{ParticipantIdentity, TrackSid}, participant::{LocalParticipant, RemoteParticipant}, @@ -292,22 +291,4 @@ impl Debug for E2eeManager { .field("enabled", &self.inner.lock().enabled) .finish_non_exhaustive() } -} - -impl data_track::DecryptionProvider for E2eeManager { - fn decrypt( - &self, - payload: data_track::EncryptedPayload, - ) -> Result { - todo!() - } -} - -impl data_track::EncryptionProvider for E2eeManager { - fn encrypt( - &self, - payload: bytes::Bytes, - ) -> Result { - todo!() - } -} +} \ No newline at end of file diff --git a/livekit/src/room/e2ee/mod.rs b/livekit/src/room/e2ee/mod.rs index 8864dc421..520959442 100644 --- a/livekit/src/room/e2ee/mod.rs +++ b/livekit/src/room/e2ee/mod.rs @@ -19,6 +19,9 @@ use self::key_provider::KeyProvider; pub mod key_provider; pub mod manager; +/// Provider implementations for data track. +mod data_track; + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum EncryptionType { #[default] From da53bbe2b0e38b1195f3c4fd5566759403fc6edf Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 17:26:41 +1100 Subject: [PATCH 079/232] Fix import paths --- livekit/src/rtc_engine/mod.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index a3ef5f184..28a91db8d 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -14,10 +14,9 @@ use std::{borrow::Cow, fmt::Debug, sync::Arc, time::Duration}; +use crate::data_track::{self, DataTrackOptions, LocalDataTrack}; use libwebrtc::prelude::*; use livekit_api::signal_client::{SignalError, SignalOptions}; -use livekit_datatrack::error::PublishError; -use livekit_datatrack::track::{DataTrack, DataTrackOptions, Local}; use livekit_protocol as proto; use livekit_runtime::{interval, Interval, JoinHandle}; use parking_lot::{RwLock, RwLockReadGuard}; @@ -29,7 +28,6 @@ use tokio::sync::{ pub use self::rtc_session::{SessionStats, INITIAL_BUFFERED_AMOUNT_LOW_THRESHOLD}; use crate::prelude::ParticipantIdentity; -use crate::{data_track, ChatMessage, E2eeManager, TranscriptionSegment}; use crate::{ id::ParticipantSid, options::TrackPublishOptions, @@ -41,6 +39,7 @@ use crate::{ }, DataPacketKind, }; +use crate::{ChatMessage, E2eeManager, TranscriptionSegment}; pub mod lk_runtime; mod peer_transport; @@ -272,10 +271,13 @@ impl RtcEngine { pub async fn publish_data_track( &self, options: DataTrackOptions, - ) -> Result, PublishError> { + ) -> Result { let (session, _r_lock) = { - let (handle, _r_lock) = - self.inner.wait_reconnection().await.map_err(|_| PublishError::Timeout)?; + let (handle, _r_lock) = self + .inner + .wait_reconnection() + .await + .map_err(|_| data_track::PublishError::Timeout)?; (handle.session.clone(), _r_lock) }; session.publish_data_track(options).await From 724cd87917d2241e41621adfb2e23e79a45d6f50 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:14:17 +1100 Subject: [PATCH 080/232] Update integration --- livekit/src/rtc_engine/rtc_session.rs | 116 +++++++++++++++++--------- 1 file changed, 78 insertions(+), 38 deletions(-) diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index cff94a595..cdd92ed7e 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -23,11 +23,12 @@ use std::{ time::Duration, }; +use crate::data_track::{internal as dt, DataTrackOptions, LocalDataTrack, PublishError}; use bytes::Bytes; use futures_util::{Stream, StreamExt}; use libwebrtc::{prelude::*, stats::RtcStats}; use livekit_api::signal_client::{SignalClient, SignalEvent, SignalEvents}; -use livekit_datatrack::{error::PublishError, track::{DataTrack, DataTrackOptions, Local}}; +// use livekit_datatrack::{error::PublishError, track::{DataTrack, DataTrackOptions, Local}}; use livekit_protocol::{self as proto}; use livekit_runtime::{sleep, JoinHandle}; use parking_lot::Mutex; @@ -374,7 +375,9 @@ struct SessionInner { e2ee_manager: Option, - dt_pub_manager: data_track::PubManager, + // Data track managers + local_dt_manager: dt::local::Manager, + remote_dt_manager: dt::remote::Manager, } /// Information about the local participant needed for outgoing @@ -415,8 +418,10 @@ struct SessionHandle { signal_task: JoinHandle<()>, rtc_task: JoinHandle<()>, dc_task: JoinHandle<()>, - dt_forward_task: JoinHandle<()>, - dt_pub_task: JoinHandle>, + local_dt_task: JoinHandle<()>, + local_dt_forward_task: JoinHandle<()>, + remote_dt_task: JoinHandle<()>, + remote_dt_forward_task: JoinHandle<()> } impl RtcSession { @@ -482,15 +487,23 @@ impl RtcSession { rtc_events::forward_dc_events(&mut lossy_dc, DataPacketKind::Lossy, rtc_emitter.clone()); rtc_events::forward_dc_events(&mut reliable_dc, DataPacketKind::Reliable, rtc_emitter); - let dt_pub_options = data_track::PubManagerOptions { + let local_dt_options = dt::local::ManagerOptions { encryption: e2ee_manager .clone() .filter(|m| m.enabled()) - .map(|m| Arc::new(m) as Arc), + .map(|m| Arc::new(m) as Arc), }; + let (local_dt_manager, local_dt_task, local_dt_events) = + dt::local::Manager::new(local_dt_options); - let (dt_pub_manager, dt_pub_task, dt_pub_signal_out, dt_pub_packet_out) = - data_track::PubManager::new(dt_pub_options); + let remote_dt_options = dt::remote::ManagerOptions { + decryption: e2ee_manager + .clone() + .filter(|m| m.enabled()) + .map(|m| Arc::new(m) as Arc), + }; + let (remote_dt_manager, remote_dt_task, remote_dt_events) = + dt::remote::Manager::new(remote_dt_options); let (close_tx, close_rx) = watch::channel(false); @@ -523,7 +536,8 @@ impl RtcSession { negotiation_queue: NegotiationQueue::new(), pending_requests: Default::default(), e2ee_manager, - dt_pub_manager, + local_dt_manager, + remote_dt_manager, }); // Start session tasks @@ -533,20 +547,24 @@ impl RtcSession { livekit_runtime::spawn(inner.clone().rtc_session_task(rtc_events, close_rx.clone())); let dc_task = livekit_runtime::spawn(inner.clone().data_channel_task(dc_events, close_rx.clone())); - let dt_forward_task = livekit_runtime::spawn(inner.clone().data_track_forward_task( - dt_pub_signal_out, - dt_pub_packet_out, - close_rx, - )); - let dt_pub_task = livekit_runtime::spawn(dt_pub_task.run()); + + let local_dt_forward_task = livekit_runtime::spawn(inner.clone().local_dt_forward_task(local_dt_events)); + let local_dt_task = livekit_runtime::spawn(local_dt_task.run()); + + let remote_dt_forward_task = livekit_runtime::spawn(inner.clone().remote_dt_forward_task(remote_dt_events)); + let remote_dt_task = livekit_runtime::spawn(remote_dt_task.run()); + + // TODO: closure. let handle = Mutex::new(Some(SessionHandle { close_tx, signal_task, rtc_task, dc_task, - dt_forward_task, - dt_pub_task, + local_dt_task, + local_dt_forward_task, + remote_dt_task, + remote_dt_forward_task })); Ok((Self { inner, handle }, join_response, session_events)) @@ -590,7 +608,14 @@ impl RtcSession { let _ = handle.rtc_task.await; let _ = handle.signal_task.await; let _ = handle.dc_task.await; - let _ = handle.dt_pub_task.await.inspect_err(|err| log::error!("{}", err)); + + _ = self.inner.local_dt_manager.handle_event(dt::local::InputEvent::Shutdown); + let _ = handle.local_dt_task.await; + let _ = handle.local_dt_forward_task.await; + + _ = self.inner.remote_dt_manager.handle_event(dt::remote::InputEvent::Shutdown); + let _ = handle.remote_dt_task.await; + let _ = handle.remote_dt_forward_task.await; } // Close the PeerConnections after the task @@ -601,7 +626,7 @@ impl RtcSession { pub async fn publish_data_track( &self, options: DataTrackOptions, - ) -> Result, PublishError> { + ) -> Result { self.inner.publish_data_track(options).await } @@ -789,26 +814,41 @@ impl SessionInner { log::debug!("closing signal_task"); } - async fn data_track_forward_task( + async fn local_dt_forward_task( self: Arc, - signal_out: impl Stream, - packet_out: impl Stream, - mut close_rx: watch::Receiver, + mut events: impl Stream + Unpin ) { - tokio::pin!(signal_out); - tokio::pin!(packet_out); - loop { - tokio::select! { - Some(message) = signal_out.next() => { - self.signal_client.send(message.into()).await - }, - Some(packet) = packet_out.next() => { - _ = self.dt_transport.send(&packet, true) - }, - _ = close_rx.changed() => break - } + while let Some(event) = events.next().await { + self.clone().forward_local_dt_event(event).await; + } + log::debug!("closing local_dt_forward_task"); + } + + async fn remote_dt_forward_task( + self: Arc, + mut events: impl Stream + Unpin, + ) { + while let Some(event) = events.next().await { + self.clone().forward_remote_dt_event(event).await; + } + log::debug!("closing remote_dt_forward_task"); + } + + async fn forward_local_dt_event( self: Arc, event: dt::local::OutputEvent) { + use dt::local::OutputEvent; + match event { + OutputEvent::PublishRequest(event) => todo!(), // forward on signal + OutputEvent::UnpublishRequest(event) => todo!(), // forward on signal + OutputEvent::PacketAvailable(event) => todo!(), // forward on transport + } + } + + async fn forward_remote_dt_event( self: Arc, event: dt::remote::OutputEvent) { + use dt::remote::OutputEvent; + match event { + OutputEvent::SubscriptionUpdated(event) => todo!(), // forward on signal + OutputEvent::TrackAvailable(event) => todo!(), // forward to room } - log::debug!("closing data_track_transport_task"); } async fn data_channel_task( @@ -1553,8 +1593,8 @@ impl SessionInner { pub async fn publish_data_track( &self, options: DataTrackOptions, - ) -> Result, PublishError> { - self.dt_pub_manager.publish_track(options).await + ) -> Result { + self.local_dt_manager.publish_track(options).await } async fn publish_data( From 9d39d32dce42e882604040229b91f6357a474108 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:29:27 +1100 Subject: [PATCH 081/232] Remote data track published event --- livekit/src/room/mod.rs | 7 ++++++- livekit/src/rtc_engine/mod.rs | 6 +++++- livekit/src/rtc_engine/rtc_session.rs | 7 +++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index baef7578d..02c3f5092 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -45,6 +45,7 @@ pub use self::{ }; pub use crate::rtc_engine::SimulateScenario; use crate::{ + data_track::RemoteDataTrack, participant::ConnectionQuality, prelude::*, registered_audio_filter_plugins, @@ -235,6 +236,8 @@ pub enum RoomEvent { ParticipantsUpdated { participants: Vec, }, + /// A remote participant published a data track. + RemoteDataTrackPublished(RemoteDataTrack) } #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -911,7 +914,9 @@ impl RoomSession { EngineEvent::RefreshToken { url, token } => { self.handle_refresh_token(url, token); } - _ => {} + EngineEvent::RemoteDataTrackPublished(track) => { + self.dispatcher.dispatch(&RoomEvent::RemoteDataTrackPublished(track)); + } } Ok(()) diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index 28a91db8d..9d866729b 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -14,7 +14,7 @@ use std::{borrow::Cow, fmt::Debug, sync::Arc, time::Duration}; -use crate::data_track::{self, DataTrackOptions, LocalDataTrack}; +use crate::data_track::{self, DataTrackOptions, LocalDataTrack, RemoteDataTrack}; use libwebrtc::prelude::*; use livekit_api::signal_client::{SignalError, SignalOptions}; use livekit_protocol as proto; @@ -186,6 +186,7 @@ pub enum EngineEvent { url: String, token: String, }, + RemoteDataTrackPublished(RemoteDataTrack) } /// Represents a running RtcSession with the ability to close the session @@ -618,6 +619,9 @@ impl EngineInner { SessionEvent::RefreshToken { url, token } => { let _ = self.engine_tx.send(EngineEvent::RefreshToken { url, token }); } + SessionEvent::RemoteDataTrackPublished(track) => { + let _ = self.engine_tx.send(EngineEvent::RemoteDataTrackPublished(track)); + } } Ok(()) } diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index cdd92ed7e..9d7ffc398 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -23,7 +23,7 @@ use std::{ time::Duration, }; -use crate::data_track::{internal as dt, DataTrackOptions, LocalDataTrack, PublishError}; +use crate::data_track::{internal as dt, DataTrackOptions, LocalDataTrack, RemoteDataTrack, PublishError}; use bytes::Bytes; use futures_util::{Stream, StreamExt}; use libwebrtc::{prelude::*, stats::RtcStats}; @@ -200,6 +200,7 @@ pub enum SessionEvent { url: String, token: String, }, + RemoteDataTrackPublished(RemoteDataTrack) } #[derive(Debug)] @@ -847,7 +848,9 @@ impl SessionInner { use dt::remote::OutputEvent; match event { OutputEvent::SubscriptionUpdated(event) => todo!(), // forward on signal - OutputEvent::TrackAvailable(event) => todo!(), // forward to room + OutputEvent::TrackAvailable(track) => { + let _ = self.emitter.send(SessionEvent::RemoteDataTrackPublished(track)); + } } } From 5e060c893db33405acf131bee431fe54b36ddc94 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:34:24 +1100 Subject: [PATCH 082/232] Forward signal messages --- livekit/src/rtc_engine/rtc_session.rs | 40 +++++++++++++++++++-------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 9d7ffc398..e0b8cae64 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -23,7 +23,9 @@ use std::{ time::Duration, }; -use crate::data_track::{internal as dt, DataTrackOptions, LocalDataTrack, RemoteDataTrack, PublishError}; +use crate::data_track::{ + internal as dt, DataTrackOptions, LocalDataTrack, PublishError, RemoteDataTrack, +}; use bytes::Bytes; use futures_util::{Stream, StreamExt}; use libwebrtc::{prelude::*, stats::RtcStats}; @@ -200,7 +202,7 @@ pub enum SessionEvent { url: String, token: String, }, - RemoteDataTrackPublished(RemoteDataTrack) + RemoteDataTrackPublished(RemoteDataTrack), } #[derive(Debug)] @@ -422,7 +424,7 @@ struct SessionHandle { local_dt_task: JoinHandle<()>, local_dt_forward_task: JoinHandle<()>, remote_dt_task: JoinHandle<()>, - remote_dt_forward_task: JoinHandle<()> + remote_dt_forward_task: JoinHandle<()>, } impl RtcSession { @@ -549,10 +551,12 @@ impl RtcSession { let dc_task = livekit_runtime::spawn(inner.clone().data_channel_task(dc_events, close_rx.clone())); - let local_dt_forward_task = livekit_runtime::spawn(inner.clone().local_dt_forward_task(local_dt_events)); + let local_dt_forward_task = + livekit_runtime::spawn(inner.clone().local_dt_forward_task(local_dt_events)); let local_dt_task = livekit_runtime::spawn(local_dt_task.run()); - let remote_dt_forward_task = livekit_runtime::spawn(inner.clone().remote_dt_forward_task(remote_dt_events)); + let remote_dt_forward_task = + livekit_runtime::spawn(inner.clone().remote_dt_forward_task(remote_dt_events)); let remote_dt_task = livekit_runtime::spawn(remote_dt_task.run()); // TODO: closure. @@ -565,7 +569,7 @@ impl RtcSession { local_dt_task, local_dt_forward_task, remote_dt_task, - remote_dt_forward_task + remote_dt_forward_task, })); Ok((Self { inner, handle }, join_response, session_events)) @@ -817,7 +821,7 @@ impl SessionInner { async fn local_dt_forward_task( self: Arc, - mut events: impl Stream + Unpin + mut events: impl Stream + Unpin, ) { while let Some(event) = events.next().await { self.clone().forward_local_dt_event(event).await; @@ -835,19 +839,31 @@ impl SessionInner { log::debug!("closing remote_dt_forward_task"); } - async fn forward_local_dt_event( self: Arc, event: dt::local::OutputEvent) { + async fn forward_local_dt_event(self: Arc, event: dt::local::OutputEvent) { use dt::local::OutputEvent; match event { - OutputEvent::PublishRequest(event) => todo!(), // forward on signal - OutputEvent::UnpublishRequest(event) => todo!(), // forward on signal + OutputEvent::PublishRequest(event) => { + self.signal_client + .send(proto::signal_request::Message::PublishDataTrackRequest(event.into())) + .await + }, + OutputEvent::UnpublishRequest(event) => { + self.signal_client + .send(proto::signal_request::Message::UnpublishDataTrackRequest(event.into())) + .await + }, OutputEvent::PacketAvailable(event) => todo!(), // forward on transport } } - async fn forward_remote_dt_event( self: Arc, event: dt::remote::OutputEvent) { + async fn forward_remote_dt_event(self: Arc, event: dt::remote::OutputEvent) { use dt::remote::OutputEvent; match event { - OutputEvent::SubscriptionUpdated(event) => todo!(), // forward on signal + OutputEvent::SubscriptionUpdated(event) => { + self.signal_client + .send(proto::signal_request::Message::UpdateDataSubscription(event.into())) + .await + } OutputEvent::TrackAvailable(track) => { let _ = self.emitter.send(SessionEvent::RemoteDataTrackPublished(track)); } From f9b64d00866a227b4e1e5b731693c432566ba350 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:38:35 +1100 Subject: [PATCH 083/232] Forward published packets on data channel --- livekit/src/rtc_engine/rtc_session.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index e0b8cae64..86e73d087 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -846,13 +846,17 @@ impl SessionInner { self.signal_client .send(proto::signal_request::Message::PublishDataTrackRequest(event.into())) .await - }, + } OutputEvent::UnpublishRequest(event) => { self.signal_client - .send(proto::signal_request::Message::UnpublishDataTrackRequest(event.into())) - .await - }, - OutputEvent::PacketAvailable(event) => todo!(), // forward on transport + .send(proto::signal_request::Message::UnpublishDataTrackRequest(event.into())) + .await + } + OutputEvent::PacketAvailable(packet) => { + _ = self.dt_transport.send(&packet, true).inspect_err(|e| { + log::error!("Failed to send data track packet over transport: {}", e) + }); + } } } From 44ccd2a9821d6abf806793b16981b6a50d732a6e Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:34:28 +1100 Subject: [PATCH 084/232] Forward input events --- livekit-datatrack/src/local/manager.rs | 2 +- livekit-datatrack/src/local/proto.rs | 21 ++++++++++++++----- livekit/src/rtc_engine/mod.rs | 6 ++++++ livekit/src/rtc_engine/rtc_session.rs | 28 +++++++++++++++++++++++--- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index dd121cf29..6738a63e7 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -61,7 +61,7 @@ pub struct PublishResultEvent { #[derive(Debug)] pub struct UnpublishEvent { /// Publisher handle of the track that was unpublished. - handle: TrackHandle, + pub handle: TrackHandle, } /// Local participant requested to publish a track. diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index 7bd0e9afc..97deed41a 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::manager::{PublishRequestEvent, UnpublishRequestEvent}; +use super::manager::{PublishRequestEvent, UnpublishEvent, UnpublishRequestEvent}; use crate::{ - dtp::TrackHandle, local::manager::PublishResultEvent, - api::{DataTrackInfo, InternalError, PublishError} + api::{DataTrackInfo, InternalError, PublishError}, + dtp::TrackHandle, + local::manager::PublishResultEvent, }; use anyhow::{anyhow, Context}; use livekit_protocol as proto; @@ -48,6 +49,16 @@ impl TryFrom for PublishResultEvent { } } +impl TryFrom for UnpublishEvent { + type Error = InternalError; + + fn try_from(msg: proto::UnpublishDataTrackResponse) -> Result { + let handle: TrackHandle = + msg.info.context("Missing info")?.pub_handle.try_into().map_err(anyhow::Error::from)?; + Ok(Self { handle }) + } +} + impl TryFrom for DataTrackInfo { type Error = InternalError; @@ -62,7 +73,7 @@ impl TryFrom for DataTrackInfo { } } -fn publish_result_from_request_response( +pub fn publish_result_from_request_response( msg: &proto::RequestResponse, ) -> Option { use proto::request_response::{Reason, Request}; @@ -80,7 +91,7 @@ fn publish_result_from_request_response( Some(event) } -fn publish_results_from_sync_state( +pub fn publish_results_from_sync_state( msg: &mut proto::SyncState, ) -> Result, InternalError> { mem::take(&mut msg.publish_data_tracks) diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index 9d866729b..516ff9a56 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -872,3 +872,9 @@ impl EngineInner { session.wait_pc_connection().await } } + +impl From for EngineError { + fn from(err: crate::data_track::InternalError) -> Self { + Self::Internal(err.to_string().into()) + } +} \ No newline at end of file diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 86e73d087..1a97b006e 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -436,7 +436,7 @@ impl RtcSession { ) -> EngineResult<(Self, proto::JoinResponse, SessionEvents)> { let (emitter, session_events) = mpsc::unbounded_channel(); - let (signal_client, join_response, signal_events) = + let (signal_client, mut join_response, signal_events) = SignalClient::connect(url, token, options.signal_options.clone()).await?; let signal_client = Arc::new(signal_client); log::debug!("received JoinResponse: {:?}", join_response); @@ -507,6 +507,9 @@ impl RtcSession { }; let (remote_dt_manager, remote_dt_task, remote_dt_events) = dt::remote::Manager::new(remote_dt_options); + if let Ok(publications_updated) = dt::remote::event_from_join(&mut join_response) { + _ = remote_dt_manager.handle_event(publications_updated.into()); + } let (close_tx, close_rx) = watch::channel(false); @@ -1100,7 +1103,10 @@ impl SessionInner { true, ); } - proto::signal_response::Message::Update(update) => { + proto::signal_response::Message::Update(mut update) => { + if let Ok(event) = dt::remote::event_from_participant_update(&mut update) { + _ = self.remote_dt_manager.handle_event(event.into()); + } let _ = self .emitter .send(SessionEvent::ParticipantUpdate { updates: update.participants }); @@ -1132,11 +1138,27 @@ impl SessionInner { }); } proto::signal_response::Message::RequestResponse(request_response) => { + if let Some(event) = dt::local::publish_result_from_request_response(&request_response) { + _ = self.local_dt_manager.handle_event(event.into()); + return Ok(()); + } let mut pending_requests = self.pending_requests.lock(); if let Some(tx) = pending_requests.remove(&request_response.request_id) { let _ = tx.send(request_response); } } + proto::signal_response::Message::PublishDataTrackResponse(publish_res) => { + let event: dt::local::PublishResultEvent = publish_res.try_into()?; + _ = self.local_dt_manager.handle_event(event.into()); + } + proto::signal_response::Message::UnpublishDataTrackResponse(unpublish_res) => { + let event: dt::local::UnpublishEvent = unpublish_res.try_into()?; + _ = self.local_dt_manager.handle_event(event.into()); + } + proto::signal_response::Message::DataTrackSubscriberHandles(subscriber_handles) => { + let event: dt::remote::SubscriberHandlesEvent = subscriber_handles.try_into()?; + _ = self.remote_dt_manager.handle_event(event.into()); + } proto::signal_response::Message::RefreshToken(ref token) => { let url = self.signal_client.url(); let _ = self.emitter.send(SessionEvent::RefreshToken { url, token: token.clone() }); @@ -1975,4 +1997,4 @@ macro_rules! make_rtc_config { } make_rtc_config!(make_rtc_config_join, proto::JoinResponse); -make_rtc_config!(make_rtc_config_reconnect, proto::ReconnectResponse); +make_rtc_config!(make_rtc_config_reconnect, proto::ReconnectResponse); \ No newline at end of file From 51ee198aeaf31645d6e12feb7bd531d3d17b902f Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:44:00 +1100 Subject: [PATCH 085/232] Simplify imports --- livekit/src/room/data_track.rs | 5 ++--- livekit/src/room/e2ee/data_track.rs | 6 +++--- livekit/src/room/mod.rs | 2 +- livekit/src/rtc_engine/mod.rs | 22 +++++++++------------- livekit/src/rtc_engine/rtc_session.rs | 15 ++++++++------- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/livekit/src/room/data_track.rs b/livekit/src/room/data_track.rs index 4f8e0bfd9..28affb196 100644 --- a/livekit/src/room/data_track.rs +++ b/livekit/src/room/data_track.rs @@ -12,6 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Re-export api types publicly, scope internal types to crate. -pub use livekit_datatrack::api::*; -pub(crate) use livekit_datatrack::internal; \ No newline at end of file +// Re-export everything in the "api" module publicly. +pub use livekit_datatrack::api::*; \ No newline at end of file diff --git a/livekit/src/room/e2ee/data_track.rs b/livekit/src/room/e2ee/data_track.rs index c653a1117..e53cd1a80 100644 --- a/livekit/src/room/e2ee/data_track.rs +++ b/livekit/src/room/e2ee/data_track.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{ - data_track::internal::{DecryptionProvider, E2eeError, EncryptedPayload, EncryptionProvider}, - E2eeManager, +use crate::E2eeManager; +use livekit_datatrack::internal::{ + DecryptionProvider, E2eeError, EncryptedPayload, EncryptionProvider, }; impl DecryptionProvider for E2eeManager { diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index 02c3f5092..a053d8b0c 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -25,6 +25,7 @@ use libwebrtc::{ use livekit_api::signal_client::{SignalOptions, SignalSdkOptions}; use livekit_protocol::observer::Dispatcher; use livekit_protocol::{self as proto, encryption}; +use livekit_datatrack::api::RemoteDataTrack; use livekit_runtime::JoinHandle; use parking_lot::RwLock; pub use proto::DisconnectReason; @@ -45,7 +46,6 @@ pub use self::{ }; pub use crate::rtc_engine::SimulateScenario; use crate::{ - data_track::RemoteDataTrack, participant::ConnectionQuality, prelude::*, registered_audio_filter_plugins, diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index 516ff9a56..d2ced6d6f 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -12,14 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{borrow::Cow, fmt::Debug, sync::Arc, time::Duration}; - -use crate::data_track::{self, DataTrackOptions, LocalDataTrack, RemoteDataTrack}; use libwebrtc::prelude::*; use livekit_api::signal_client::{SignalError, SignalOptions}; +use livekit_datatrack::api::{DataTrackOptions, LocalDataTrack, PublishError, RemoteDataTrack}; use livekit_protocol as proto; use livekit_runtime::{interval, Interval, JoinHandle}; use parking_lot::{RwLock, RwLockReadGuard}; +use std::{borrow::Cow, fmt::Debug, sync::Arc, time::Duration}; use thiserror::Error; use tokio::sync::{ mpsc, oneshot, Mutex as AsyncMutex, Notify, RwLock as AsyncRwLock, @@ -186,7 +185,7 @@ pub enum EngineEvent { url: String, token: String, }, - RemoteDataTrackPublished(RemoteDataTrack) + RemoteDataTrackPublished(RemoteDataTrack), } /// Represents a running RtcSession with the ability to close the session @@ -272,13 +271,10 @@ impl RtcEngine { pub async fn publish_data_track( &self, options: DataTrackOptions, - ) -> Result { + ) -> Result { let (session, _r_lock) = { - let (handle, _r_lock) = self - .inner - .wait_reconnection() - .await - .map_err(|_| data_track::PublishError::Timeout)?; + let (handle, _r_lock) = + self.inner.wait_reconnection().await.map_err(|_| PublishError::Timeout)?; (handle.session.clone(), _r_lock) }; session.publish_data_track(options).await @@ -873,8 +869,8 @@ impl EngineInner { } } -impl From for EngineError { - fn from(err: crate::data_track::InternalError) -> Self { +impl From for EngineError { + fn from(err: livekit_datatrack::api::InternalError) -> Self { Self::Internal(err.to_string().into()) } -} \ No newline at end of file +} diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 1a97b006e..1989f8bcb 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -23,14 +23,14 @@ use std::{ time::Duration, }; -use crate::data_track::{ - internal as dt, DataTrackOptions, LocalDataTrack, PublishError, RemoteDataTrack, -}; use bytes::Bytes; use futures_util::{Stream, StreamExt}; use libwebrtc::{prelude::*, stats::RtcStats}; use livekit_api::signal_client::{SignalClient, SignalEvent, SignalEvents}; -// use livekit_datatrack::{error::PublishError, track::{DataTrack, DataTrackOptions, Local}}; +use livekit_datatrack::{ + api::{DataTrackOptions, LocalDataTrack, PublishError, RemoteDataTrack}, + internal as dt, +}; use livekit_protocol::{self as proto}; use livekit_runtime::{sleep, JoinHandle}; use parking_lot::Mutex; @@ -44,7 +44,6 @@ use tokio::sync::{mpsc, oneshot, watch, Notify}; use super::{rtc_events, EngineError, EngineOptions, EngineResult, SimulateScenario}; use crate::{ - data_track, id::ParticipantIdentity, utils::{ ttl_map::TtlMap, @@ -1138,7 +1137,9 @@ impl SessionInner { }); } proto::signal_response::Message::RequestResponse(request_response) => { - if let Some(event) = dt::local::publish_result_from_request_response(&request_response) { + if let Some(event) = + dt::local::publish_result_from_request_response(&request_response) + { _ = self.local_dt_manager.handle_event(event.into()); return Ok(()); } @@ -1997,4 +1998,4 @@ macro_rules! make_rtc_config { } make_rtc_config!(make_rtc_config_join, proto::JoinResponse); -make_rtc_config!(make_rtc_config_reconnect, proto::ReconnectResponse); \ No newline at end of file +make_rtc_config!(make_rtc_config_reconnect, proto::ReconnectResponse); From 0af0cb8441300e2d11964163d168d8230b702f55 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:01:25 +1100 Subject: [PATCH 086/232] Rename --- livekit-datatrack/src/local/manager.rs | 4 ++-- livekit-datatrack/src/remote/manager.rs | 4 ++-- livekit/src/rtc_engine/rtc_session.rs | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 6738a63e7..9c4841cff 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -148,8 +148,8 @@ impl Manager { (manager, task, event_out_stream) } - /// Handles an external event. - pub fn handle_event(&self, event: InputEvent) -> Result<(), InternalError> { + /// Sends an input event to the manager's task to be processed. + pub fn send(&self, event: InputEvent) -> Result<(), InternalError> { Ok(self.event_in_tx.try_send(event).context("Failed to handle input event")?) } diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 9a0c3c819..f6e1e62c7 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -131,8 +131,8 @@ impl Manager { (manager, task, event_out_stream) } - /// Handles an external event. - pub fn handle_event(&self, event: InputEvent) -> Result<(), InternalError> { + /// Sends an input event to the manager's task to be processed. + pub fn send(&self, event: InputEvent) -> Result<(), InternalError> { Ok(self.event_in_tx.try_send(event).context("Failed to send input event")?) } diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 1989f8bcb..86e471585 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -507,7 +507,7 @@ impl RtcSession { let (remote_dt_manager, remote_dt_task, remote_dt_events) = dt::remote::Manager::new(remote_dt_options); if let Ok(publications_updated) = dt::remote::event_from_join(&mut join_response) { - _ = remote_dt_manager.handle_event(publications_updated.into()); + _ = remote_dt_manager.send(publications_updated.into()); } let (close_tx, close_rx) = watch::channel(false); @@ -616,11 +616,11 @@ impl RtcSession { let _ = handle.signal_task.await; let _ = handle.dc_task.await; - _ = self.inner.local_dt_manager.handle_event(dt::local::InputEvent::Shutdown); + _ = self.inner.local_dt_manager.send(dt::local::InputEvent::Shutdown); let _ = handle.local_dt_task.await; let _ = handle.local_dt_forward_task.await; - _ = self.inner.remote_dt_manager.handle_event(dt::remote::InputEvent::Shutdown); + _ = self.inner.remote_dt_manager.send(dt::remote::InputEvent::Shutdown); let _ = handle.remote_dt_task.await; let _ = handle.remote_dt_forward_task.await; } @@ -1104,7 +1104,7 @@ impl SessionInner { } proto::signal_response::Message::Update(mut update) => { if let Ok(event) = dt::remote::event_from_participant_update(&mut update) { - _ = self.remote_dt_manager.handle_event(event.into()); + _ = self.remote_dt_manager.send(event.into()); } let _ = self .emitter @@ -1140,7 +1140,7 @@ impl SessionInner { if let Some(event) = dt::local::publish_result_from_request_response(&request_response) { - _ = self.local_dt_manager.handle_event(event.into()); + _ = self.local_dt_manager.send(event.into()); return Ok(()); } let mut pending_requests = self.pending_requests.lock(); @@ -1150,15 +1150,15 @@ impl SessionInner { } proto::signal_response::Message::PublishDataTrackResponse(publish_res) => { let event: dt::local::PublishResultEvent = publish_res.try_into()?; - _ = self.local_dt_manager.handle_event(event.into()); + _ = self.local_dt_manager.send(event.into()); } proto::signal_response::Message::UnpublishDataTrackResponse(unpublish_res) => { let event: dt::local::UnpublishEvent = unpublish_res.try_into()?; - _ = self.local_dt_manager.handle_event(event.into()); + _ = self.local_dt_manager.send(event.into()); } proto::signal_response::Message::DataTrackSubscriberHandles(subscriber_handles) => { let event: dt::remote::SubscriberHandlesEvent = subscriber_handles.try_into()?; - _ = self.remote_dt_manager.handle_event(event.into()); + _ = self.remote_dt_manager.send(event.into()); } proto::signal_response::Message::RefreshToken(ref token) => { let url = self.signal_client.url(); From 37aa225912eb0f6cc4c63551c0429000877db8e5 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:15:50 +1100 Subject: [PATCH 087/232] Remove unused type --- livekit/src/rtc_engine/rtc_session.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 86e471585..fb38a3d29 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -482,6 +482,7 @@ impl RtcSession { ..DataChannelInit::default() }, )?; + dt_transport.on_message(|data| {}.into()); // Forward events received inside the signaling thread to our rtc channel rtc_events::forward_pc_events(&mut publisher_pc, rtc_emitter.clone()); @@ -1971,8 +1972,6 @@ impl SessionInner { } } -struct DataTrackTransportTask {} - macro_rules! make_rtc_config { ($fncname:ident, $proto:ty) => { fn $fncname(value: $proto, mut config: RtcConfiguration) -> RtcConfiguration { From a9d19235a1b2b3c83b39f77703919498fbb1e207 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:31:17 +1100 Subject: [PATCH 088/232] Forward incoming packets to manager --- livekit-datatrack/src/remote/manager.rs | 1 + livekit/src/rtc_engine/rtc_session.rs | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index f6e1e62c7..2a1ef186d 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -107,6 +107,7 @@ pub struct ManagerOptions { } /// Manager for remote data tracks. +#[derive(Debug, Clone)] pub struct Manager { event_in_tx: mpsc::Sender, } diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index fb38a3d29..769ebf0e6 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -474,7 +474,7 @@ impl RtcSession { DataChannelInit { ordered: true, ..DataChannelInit::default() }, )?; - let dt_transport = publisher_pc.peer_connection().create_data_channel( + let mut dt_transport = publisher_pc.peer_connection().create_data_channel( DATA_TRACK_DC_LABEL, DataChannelInit { ordered: false, @@ -482,7 +482,6 @@ impl RtcSession { ..DataChannelInit::default() }, )?; - dt_transport.on_message(|data| {}.into()); // Forward events received inside the signaling thread to our rtc channel rtc_events::forward_pc_events(&mut publisher_pc, rtc_emitter.clone()); @@ -507,6 +506,7 @@ impl RtcSession { }; let (remote_dt_manager, remote_dt_task, remote_dt_events) = dt::remote::Manager::new(remote_dt_options); + forward_incoming_dt_packets(&mut dt_transport, remote_dt_manager.clone()); if let Ok(publications_updated) = dt::remote::event_from_join(&mut join_response) { _ = remote_dt_manager.send(publications_updated.into()); } @@ -1972,6 +1972,19 @@ impl SessionInner { } } +/// Sets up forwarding of incoming data track packets to the manager as input events. +pub fn forward_incoming_dt_packets(dc: &mut DataChannel, manager: dt::remote::Manager) { + let on_message: libwebrtc::data_channel::OnMessage = Box::new(move |buffer: DataBuffer| { + if !buffer.binary { + log::error!("Received non-binary message"); + return; + } + let packet: Bytes = buffer.data.to_vec().into(); // TODO: avoid clone if possible + _ = manager.send(packet.into()); + }); + dc.on_message(on_message.into()); +} + macro_rules! make_rtc_config { ($fncname:ident, $proto:ty) => { fn $fncname(value: $proto, mut config: RtcConfiguration) -> RtcConfiguration { From 18b9e93f4fbbb803791525979535bf3b8974c9ec Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:23:35 +1100 Subject: [PATCH 089/232] Use future core trait, pin stream --- Cargo.lock | 2 ++ livekit-datatrack/Cargo.toml | 1 + livekit-datatrack/src/remote/mod.rs | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 888b346e1..3f8544e2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1336,6 +1336,7 @@ version = "0.1.0" dependencies = [ "anyhow", "env_logger 0.11.8", + "futures-util", "livekit", "log", "tokio", @@ -3077,6 +3078,7 @@ dependencies = [ "anyhow", "bytes", "from_variants", + "futures-core", "futures-util", "livekit-protocol", "livekit-runtime", diff --git a/livekit-datatrack/Cargo.toml b/livekit-datatrack/Cargo.toml index 42bb5b419..9ff49b38f 100644 --- a/livekit-datatrack/Cargo.toml +++ b/livekit-datatrack/Cargo.toml @@ -13,6 +13,7 @@ log = { version = "0.4.28" } thiserror = "2.0.17" tokio = { version = "1.48.0", default-features = false, features = ["sync"] } futures-util = { version = "0.3", default-features = false, features = ["sink"] } +futures-core = "0.3.31" bytes = "1.10.1" from_variants = "1.0.2" tokio-stream = { version = "0.1.17", features = ["sync"] } diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 08a4cffef..6db14fa4d 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -70,7 +70,7 @@ impl DataTrack { let frame_stream = BroadcastStream::new(frame_rx).filter_map(|result| async move { result.ok() }); - Ok(frame_stream) + Ok(Box::pin(frame_stream)) } /// Whether or not the track is still published. From 5cb900cad1232b9bb747ebedfb696cab24db4900 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:23:52 +1100 Subject: [PATCH 090/232] Expose data tracks types in prelude --- livekit/src/prelude.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/livekit/src/prelude.rs b/livekit/src/prelude.rs index 3e6bda23b..43b311fe8 100644 --- a/livekit/src/prelude.rs +++ b/livekit/src/prelude.rs @@ -15,6 +15,10 @@ pub use livekit_protocol::AudioTrackFeature; pub use crate::{ + data_track::{ + DataTrackFrame, DataTrackFrameBuilder, DataTrackInfo, DataTrackOptions, LocalDataTrack, + PublishError, PublishFrameError, PublishFrameErrorReason, RemoteDataTrack, + }, id::*, participant::{ ConnectionQuality, DisconnectReason, LocalParticipant, Participant, PerformRpcData, From 2a83de495c551bec07bd47d2dd40676930bccc87 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:29:43 +1100 Subject: [PATCH 091/232] Update examples --- examples/data_track/Cargo.toml | 4 +-- examples/data_track/src/publisher.rs | 25 +++++++-------- examples/data_track/src/subscriber.rs | 44 ++++++++++++++++++++++++-- livekit-datatrack/src/local/manager.rs | 2 +- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/examples/data_track/Cargo.toml b/examples/data_track/Cargo.toml index f49ab8622..2bb6022ca 100644 --- a/examples/data_track/Cargo.toml +++ b/examples/data_track/Cargo.toml @@ -7,15 +7,15 @@ publish = false [dependencies] tokio = { version = "1", features = ["full"] } livekit = { workspace = true, features = ["rustls-tls-native-roots"] } +futures-util = { version = "0.3", default-features = false, features = ["sink"] } anyhow = "1.0.100" log = "0.4.26" env_logger = "0.11.7" - [[bin]] name = "publisher" path = "src/publisher.rs" [[bin]] name = "subscriber" -path = "src/subscriber.rs" +path = "src/subscriber.rs" \ No newline at end of file diff --git a/examples/data_track/src/publisher.rs b/examples/data_track/src/publisher.rs index 706779a89..8ddcc0052 100644 --- a/examples/data_track/src/publisher.rs +++ b/examples/data_track/src/publisher.rs @@ -1,12 +1,6 @@ use anyhow::Result; -use livekit::{ - data_track::{DataTrack, DataTrackFrameBuilder, DataTrackOptions, Local}, - prelude::*, -}; -use std::{ - env, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; +use livekit::prelude::*; +use std::{env, time::Duration}; use tokio::{signal, time}; #[tokio::main] @@ -16,9 +10,9 @@ async fn main() -> Result<()> { let url = env::var("LIVEKIT_URL").expect("LIVEKIT_URL is not set"); let token = env::var("LIVEKIT_TOKEN").expect("LIVEKIT_TOKEN is not set"); - let (room, rx) = Room::connect(&url, &token, RoomOptions::default()).await?; + let (room, _) = Room::connect(&url, &token, RoomOptions::default()).await?; - let options = DataTrackOptions::with_name("brightness"); + let options = DataTrackOptions::with_name("my_sensor_data"); let track = room.local_participant().publish_data_track(options).await?; tokio::select! { @@ -28,13 +22,18 @@ async fn main() -> Result<()> { Ok(()) } -async fn publish_frames(track: DataTrack) { +async fn read_sensor() -> Vec { + vec![0xFA; 256] +} + +async fn publish_frames(track: LocalDataTrack) { loop { - let frame = DataTrackFrameBuilder::new(vec![0xFA; 256]); + log::info!("Publishing frame"); + let frame = DataTrackFrameBuilder::new(read_sensor().await); track .publish(frame.build()) .inspect_err(|err| println!("Failed to publish frame: {}", err)) .ok(); time::sleep(Duration::from_millis(500)).await } -} +} \ No newline at end of file diff --git a/examples/data_track/src/subscriber.rs b/examples/data_track/src/subscriber.rs index fde05e1a0..f3669e64e 100644 --- a/examples/data_track/src/subscriber.rs +++ b/examples/data_track/src/subscriber.rs @@ -1,6 +1,46 @@ use anyhow::Result; +use futures_util::StreamExt; +use livekit::prelude::*; +use std::env; +use tokio::{signal, sync::mpsc::UnboundedReceiver}; #[tokio::main] async fn main() -> Result<()> { - panic!("Not implemented") -} \ No newline at end of file + env_logger::init(); + + let url = env::var("LIVEKIT_URL").expect("LIVEKIT_URL is not set"); + let token = env::var("LIVEKIT_TOKEN").expect("LIVEKIT_TOKEN is not set"); + + let (_, rx) = Room::connect(&url, &token, RoomOptions::default()).await?; + + tokio::select! { + Some(track) = wait_for_publication(rx) => subscribe(track).await?, + _ = signal::ctrl_c() => {} + } + Ok(()) +} + +/// Waits for the first data track to be published and returns it. +async fn wait_for_publication(mut rx: UnboundedReceiver) -> Option { + while let Some(event) = rx.recv().await { + match event { + RoomEvent::RemoteDataTrackPublished(track) => return Some(track), + _ => continue, + } + } + None +} + +/// Subscribes to the given data track and logs received frames. +async fn subscribe(track: RemoteDataTrack) -> Result<()> { + log::info!( + "Subscribing to '{}' published by '{}'", + track.info().name(), + track.publisher_identity() + ); + let mut frame_steam = track.subscribe().await?; + while let Some(frame) = frame_steam.next().await { + log::info!("Received {} bytes", frame.payload().len()); + } + Ok(()) +} diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 9c4841cff..63cd2887a 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -22,7 +22,7 @@ use crate::{ use anyhow::{anyhow, Context}; use bytes::Bytes; use from_variants::FromVariants; -use futures_util::Stream; +use futures_core::Stream; use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::sync::{mpsc, oneshot, watch}; use tokio::time; From adcb23c10f4eaba5db68feb24c97820f55d85ce8 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:06:42 +1100 Subject: [PATCH 092/232] Simplify frame construction --- examples/data_track/src/publisher.rs | 5 +-- livekit-datatrack/src/frame.rs | 52 +++++++++++++++++----------- livekit-datatrack/src/local/mod.rs | 3 +- livekit/src/prelude.rs | 2 +- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/examples/data_track/src/publisher.rs b/examples/data_track/src/publisher.rs index 8ddcc0052..83d5a0eaa 100644 --- a/examples/data_track/src/publisher.rs +++ b/examples/data_track/src/publisher.rs @@ -23,15 +23,16 @@ async fn main() -> Result<()> { } async fn read_sensor() -> Vec { + // Dynamically read some sensor data... vec![0xFA; 256] } async fn publish_frames(track: LocalDataTrack) { loop { log::info!("Publishing frame"); - let frame = DataTrackFrameBuilder::new(read_sensor().await); + let frame = read_sensor().await.into(); track - .publish(frame.build()) + .publish(frame) .inspect_err(|err| println!("Failed to publish frame: {}", err)) .ok(); time::sleep(Duration::from_millis(500)).await diff --git a/livekit-datatrack/src/frame.rs b/livekit-datatrack/src/frame.rs index e79462469..769e80a5f 100644 --- a/livekit-datatrack/src/frame.rs +++ b/livekit-datatrack/src/frame.rs @@ -14,48 +14,60 @@ use bytes::Bytes; -/// Frame published on a data track containing metadata and a payload. -/// -/// Construct using [`DataTrackFrameBuilder`]. -/// -#[derive(Debug, Clone)] +/// Application-level frame published to a data track. +#[derive(Debug, Clone, Default)] pub struct DataTrackFrame { pub(crate) payload: Bytes, pub(crate) user_timestamp: Option, } impl DataTrackFrame { - /// Get the frame's payload. + /// Returns the frame's payload. pub fn payload(&self) -> Bytes { self.payload.clone() // Cheap clone } - /// Get the frame's user timestamp, if attached. + /// Returns the frame's user timestamp, if one is associated. pub fn user_timestamp(&self) -> Option { self.user_timestamp } } -/// Constructs a [`DataTrackFrame`]. -#[derive(Default)] -pub struct DataTrackFrameBuilder { - payload: Bytes, - user_timestamp: Option, -} - -impl DataTrackFrameBuilder { +impl DataTrackFrame { + /// Creates a data track frame with the given bytes. pub fn new(payload: impl Into) -> Self { Self { payload: payload.into(), ..Default::default() } } - pub fn user_timestamp(mut self, user_timestamp: u64) -> Self { - self.user_timestamp = Some(user_timestamp); + /// Associates a user timestamp with the frame. + pub fn with_user_timestamp(&mut self, value: u64) -> &mut Self { + self.user_timestamp = Some(value); self } +} + +// MARK: - From implementations + +impl From for DataTrackFrame { + fn from(bytes: Bytes) -> Self { + Self { payload: bytes, ..Default::default() } + } +} + +impl From<&'static [u8]> for DataTrackFrame { + fn from(slice: &'static [u8]) -> Self { + Self { payload: slice.into(), ..Default::default() } + } +} - pub fn build(self) -> DataTrackFrame { - DataTrackFrame { payload: self.payload, user_timestamp: self.user_timestamp } +impl From> for DataTrackFrame { + fn from(vec: Vec) -> Self { + Self { payload: vec.into(), ..Default::default() } } } -// TODO: just show payload length in debug. +impl From> for DataTrackFrame { + fn from(slice: Box<[u8]>) -> Self { + Self { payload: slice.into(), ..Default::default() } + } +} \ No newline at end of file diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index c75ccb72d..de3316343 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -48,8 +48,7 @@ impl DataTrack { impl DataTrack { /// Publish a frame onto the track. - pub fn publish(&self, frame: impl Into) -> Result<(), PublishFrameError> { - let frame = frame.into(); + pub fn publish(&self, frame: DataTrackFrame) -> Result<(), PublishFrameError> { if !self.is_published() { return Err(PublishFrameError::new(frame, PublishFrameErrorReason::TrackUnpublished)); } diff --git a/livekit/src/prelude.rs b/livekit/src/prelude.rs index 43b311fe8..4c8af91cd 100644 --- a/livekit/src/prelude.rs +++ b/livekit/src/prelude.rs @@ -16,7 +16,7 @@ pub use livekit_protocol::AudioTrackFeature; pub use crate::{ data_track::{ - DataTrackFrame, DataTrackFrameBuilder, DataTrackInfo, DataTrackOptions, LocalDataTrack, + DataTrackFrame, DataTrackInfo, DataTrackOptions, LocalDataTrack, PublishError, PublishFrameError, PublishFrameErrorReason, RemoteDataTrack, }, id::*, From 1fb9174808e8c19704eb9f26bf2c459a158c2dbe Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:54:55 +1100 Subject: [PATCH 093/232] Relocate packetizer --- livekit-datatrack/src/dtp/mod.rs | 3 +-- livekit-datatrack/src/local/manager.rs | 4 ++-- livekit-datatrack/src/local/mod.rs | 1 + livekit-datatrack/src/{dtp => local}/packetizer.rs | 2 +- livekit-datatrack/src/local/pipeline.rs | 10 ++++------ 5 files changed, 9 insertions(+), 11 deletions(-) rename livekit-datatrack/src/{dtp => local}/packetizer.rs (99%) diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index fc936a67d..65e87efad 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -14,13 +14,12 @@ mod deserialize; mod packet; -mod packetizer; mod serialize; mod time; mod track_handle; pub use deserialize::*; pub use packet::*; -pub use packetizer::*; +pub use time::*; pub use serialize::*; pub use track_handle::*; diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 63cd2887a..8f9a0ccfb 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{pipeline::LocalTrackTask, LocalTrackInner}; +use super::{packetizer::Packetizer, pipeline::LocalTrackTask, LocalTrackInner}; use crate::{ api::{DataTrackInfo, DataTrackOptions, InternalError, PublishError}, dtp::{self, TrackHandle}, @@ -277,7 +277,7 @@ impl ManagerTask { let task = LocalTrackTask { // TODO: handle cancellation - packetizer: dtp::Packetizer::new(info.handle, Self::TRANSPORT_MTU), + packetizer: Packetizer::new(info.handle, Self::TRANSPORT_MTU), encryption: self.encryption.clone(), info: info.clone(), frame_rx, diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index de3316343..8c5c0d762 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -25,6 +25,7 @@ pub(crate) mod manager; pub(crate) mod proto; mod pipeline; +mod packetizer; /// Data track published by the local participant. pub type LocalDataTrack = DataTrack; diff --git a/livekit-datatrack/src/dtp/packetizer.rs b/livekit-datatrack/src/local/packetizer.rs similarity index 99% rename from livekit-datatrack/src/dtp/packetizer.rs rename to livekit-datatrack/src/local/packetizer.rs index c6c729b9d..5aa7672c6 100644 --- a/livekit-datatrack/src/dtp/packetizer.rs +++ b/livekit-datatrack/src/local/packetizer.rs @@ -14,7 +14,7 @@ use crate::{ dtp::{ - time::{Clock, Timestamp}, + Clock, Timestamp, Dtp, E2ee, Header, TrackHandle, }, utils::{BytesChunkExt, Counter}, diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index e3eb9026a..b46e20f85 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::packetizer::{Packetizer, PacketizerFrame}; use crate::{ api::{DataTrackFrame, DataTrackInfo, InternalError}, dtp, @@ -24,7 +25,7 @@ use tokio::sync::{mpsc, watch}; /// Task responsible for operating an individual published data track. pub(super) struct LocalTrackTask { - pub packetizer: dtp::Packetizer, + pub packetizer: Packetizer, pub encryption: Option>, pub info: Arc, pub state_rx: watch::Receiver, @@ -66,11 +67,8 @@ impl LocalTrackTask { frame.payload = encrypted_payload.payload; } - let frame = dtp::PacketizerFrame { - payload: frame.payload, - e2ee, - user_timestamp: frame.user_timestamp, - }; + let frame = + PacketizerFrame { payload: frame.payload, e2ee, user_timestamp: frame.user_timestamp }; let packets = self.packetizer.packetize(frame).context("Failed to packetize frame")?; for packet in packets { let serialized = packet.serialize(); From 77169123d12404915cdbfb5c55ca5dc1978881be Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 16:04:08 +1100 Subject: [PATCH 094/232] Skelton for depacketizer --- livekit-datatrack/src/remote/depacketizer.rs | 31 ++++++++++++++++++++ livekit-datatrack/src/remote/manager.rs | 3 +- livekit-datatrack/src/remote/mod.rs | 1 + livekit-datatrack/src/remote/pipeline.rs | 10 ++++--- 4 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 livekit-datatrack/src/remote/depacketizer.rs diff --git a/livekit-datatrack/src/remote/depacketizer.rs b/livekit-datatrack/src/remote/depacketizer.rs new file mode 100644 index 000000000..63438cfaf --- /dev/null +++ b/livekit-datatrack/src/remote/depacketizer.rs @@ -0,0 +1,31 @@ +// Copyright 2025 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 crate::{dtp::Dtp, frame::DataTrackFrame}; + +pub struct Depacketizer; + +// TODO: implement depacketizer + +impl Depacketizer { + /// Creates a new depacketizer. + pub fn new() -> Self { + Self + } + + /// Push a packet into the depacketizer, returning a complete frame if one is available. + pub fn push(&mut self, dtp: Dtp) -> Option { + todo!() + } +} \ No newline at end of file diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 2a1ef186d..8fdbaffc5 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{RemoteDataTrack, RemoteTrackInner}; +use super::{depacketizer::Depacketizer, RemoteDataTrack, RemoteTrackInner}; use crate::{ api::{DataTrackFrame, DataTrackInfo, InternalError, SubscribeError}, dtp::{Dtp, TrackHandle}, @@ -288,6 +288,7 @@ impl ManagerTask { let (frame_tx, frame_rx) = broadcast::channel(4); let track_task = RemoteTrackTask { + depacketizer: Depacketizer::new(), decryption: self.decryption.clone(), info: descriptor.info.clone(), state_rx: descriptor.state_tx.subscribe(), diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 6db14fa4d..0ed567a7f 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -25,6 +25,7 @@ use tokio_stream::{wrappers::BroadcastStream, Stream}; pub(crate) mod manager; pub(crate) mod proto; +mod depacketizer; mod pipeline; /// Data track published by a remote participant. diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 7c26c8582..4123859cb 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -12,17 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::manager::TrackState; +use super::{depacketizer::Depacketizer, manager::TrackState}; use crate::{ api::{DataTrackFrame, DataTrackInfo}, - e2ee::{DecryptionProvider, EncryptedPayload}, dtp::Dtp, + e2ee::{DecryptionProvider, EncryptedPayload}, }; use std::sync::Arc; use tokio::sync::{broadcast, mpsc, watch}; pub(super) struct RemoteTrackTask { - // pub depacketizer: dtp::Depacketizer, + pub depacketizer: Depacketizer, pub decryption: Option>, pub info: Arc, pub state_rx: watch::Receiver, @@ -71,6 +71,8 @@ impl RemoteTrackTask { }; dtp.payload = decrypted_payload; } - // TODO: depacketize, emit complete frame + if let Some(frame) = self.depacketizer.push(dtp) { + _ = self.frame_tx.send(frame); + } } } From 64eb934f4053cf121a2f2e7eb0fc19f97fc3f81b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:10:52 +1100 Subject: [PATCH 095/232] Basic manager tests --- livekit-datatrack/src/local/manager.rs | 75 +++++++++++++++++++++++++ livekit-datatrack/src/remote/manager.rs | 18 ++++++ 2 files changed, 93 insertions(+) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 8f9a0ccfb..6e9b56e03 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -329,3 +329,78 @@ impl ManagerTask { /// MTU of the transport const TRANSPORT_MTU: usize = 16_000; } + +#[cfg(test)] +mod tests { + use super::*; + use crate::dtp::Dtp; + use futures_util::StreamExt; + use livekit_runtime::sleep; + use rstest::*; + + #[tokio::test] + async fn test_task_shutdown() { + let options = ManagerOptions { encryption: None }; + let (manager, manager_task, _) = Manager::new(options); + + let join_handle = livekit_runtime::spawn(manager_task.run()); + _ = manager.send(InputEvent::Shutdown); + + time::timeout(Duration::from_secs(1), join_handle).await.unwrap(); + } + + #[rstest] + #[case("my_track", 10, 256)] + #[tokio::test] + async fn test_publish( + #[case] name: String, + #[case] packet_count: usize, + #[case] payload_size: usize, + ) { + let options = ManagerOptions { encryption: None }; + let (manager, manager_task, mut output_events) = Manager::new(options); + livekit_runtime::spawn(manager_task.run()); + + let handle_events = async { + let mut packets_sent = 0; + while let Some(event) = output_events.next().await { + match event { + OutputEvent::PublishRequest(event) => { + // SFU accepts publication + let info = DataTrackInfo { + sid: "DTR_1234".into(), + handle: 1u32.try_into().unwrap(), + name: event.name, + uses_e2ee: event.uses_e2ee, + }; + let input_event = + PublishResultEvent { handle: event.handle, result: Ok(info) }; + _ = manager.send(input_event.into()); + } + OutputEvent::PacketAvailable(packet) => { + let payload = Dtp::deserialize(packet).unwrap().payload; + assert_eq!(payload.len(), payload_size); + packets_sent += 1; + } + OutputEvent::UnpublishRequest(event) => { + assert_eq!(event.handle, 1u32.try_into().unwrap()); + assert_eq!(packets_sent, packet_count); + break; + } + } + } + }; + let publish_track = async { + let track_options = DataTrackOptions::with_name(name.clone()); + let track = manager.publish_track(track_options).await.unwrap(); + for _ in 0..packet_count { + track.publish(vec![0xFA; payload_size].into()).unwrap(); + sleep(Duration::from_millis(10)).await; + } + // Only reference to track dropped here (unpublish) + }; + time::timeout(Duration::from_secs(1), async { tokio::join!(publish_track, handle_events) }) + .await + .unwrap(); + } +} diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 8fdbaffc5..38d7318f2 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -350,3 +350,21 @@ impl ManagerTask { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + use tokio::time; + + #[tokio::test] + async fn test_task_shutdown() { + let options = ManagerOptions { decryption: None }; + let (manager, manager_task, _) = Manager::new(options); + + let join_handle = livekit_runtime::spawn(manager_task.run()); + _ = manager.send(InputEvent::Shutdown); + + time::timeout(Duration::from_secs(1), join_handle).await.unwrap(); + } +} From 42a4b4b3b02da30f14a2d516856a96bf06bbdbe3 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:17:41 +1100 Subject: [PATCH 096/232] Remote track task spawn and join --- livekit-datatrack/src/remote/manager.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 38d7318f2..80d2d0157 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -148,7 +148,7 @@ impl Manager { struct Descriptor { info: Arc, state_tx: watch::Sender, - state: DescriptorState, + state: DescriptorState } #[derive(Debug)] @@ -161,6 +161,7 @@ enum DescriptorState { Subscribed { packet_tx: mpsc::Sender, frame_tx: broadcast::Sender, + join_handle: livekit_runtime::JoinHandle<()> }, } @@ -187,7 +188,7 @@ impl ManagerTask { InputEvent::Shutdown => break, } } - self.shutdown(); + self.shutdown().await; } fn handle_publications_updated(&mut self, event: PublicationsUpdatedEvent) { @@ -263,7 +264,7 @@ impl ManagerTask { DescriptorState::Pending { result_txs } => { result_txs.push(event.result_tx); } - DescriptorState::Subscribed { packet_tx: _, frame_tx } => { + DescriptorState::Subscribed { frame_tx, .. } => { let frame_rx = frame_tx.subscribe(); _ = event.result_tx.send(Ok(frame_rx)) } @@ -296,9 +297,9 @@ impl ManagerTask { frame_tx: frame_tx.clone(), event_out_tx: self.event_out_tx.downgrade(), }; - // TODO: spawn task + let join_handle = livekit_runtime::spawn(track_task.run()); - descriptor.state = DescriptorState::Subscribed { packet_tx, frame_tx }; + descriptor.state = DescriptorState::Subscribed { packet_tx, frame_tx, join_handle }; self.sub_handles.insert(handle, sid); // TODO: send completion @@ -306,7 +307,7 @@ impl ManagerTask { // result_tx.send(Ok(frame_rx.resubscribe())); // } } - DescriptorState::Subscribed { packet_tx: _, frame_tx: _ } => { + DescriptorState::Subscribed { .. } => { log::warn!("Handle reassignment not implemented"); } } @@ -328,7 +329,7 @@ impl ManagerTask { log::warn!("Missing descriptor"); return; }; - let DescriptorState::Subscribed { packet_tx, frame_tx: _ } = &descriptor.state else { + let DescriptorState::Subscribed { packet_tx, .. } = &descriptor.state else { log::warn!("Received packet for track {} without subscription", descriptor.info.sid); return; }; @@ -336,17 +337,20 @@ impl ManagerTask { } /// Performs cleanup before the task ends. - fn shutdown(self) { + async fn shutdown(self) { for (_, descriptor) in self.descriptors { + _ = descriptor.state_tx.send(TrackState::Unpublished); match descriptor.state { - DescriptorState::Available | DescriptorState::Subscribed { .. } => {} + DescriptorState::Available { .. } => {} DescriptorState::Pending { result_txs } => { for result_tx in result_txs { _ = result_tx.send(Err(SubscribeError::Disconnected)); } } + DescriptorState::Subscribed { join_handle, .. } => { + join_handle.await + } } - _ = descriptor.state_tx.send(TrackState::Unpublished); } } } From 1f11d1466981ae212e119a5693e2d1de2ea78e43 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:22:23 +1100 Subject: [PATCH 097/232] Local join track task --- livekit-datatrack/src/local/manager.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 6e9b56e03..32324ac87 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -186,7 +186,10 @@ enum Descriptor { /// /// The associated channel is used to send state updates to the track's task. /// - Active(watch::Sender), + Active { + state_tx: watch::Sender, + join_handle: livekit_runtime::JoinHandle<()> + }, } pub struct ManagerTask { @@ -207,7 +210,7 @@ impl ManagerTask { let Err(err) = self.handle_event(event) else { continue }; log::error!("Failed to handle input event: {}", err); } - self.shutdown(); + self.shutdown().await; } fn handle_event(&mut self, event: InputEvent) -> Result<(), InternalError> { @@ -284,8 +287,8 @@ impl ManagerTask { state_rx, event_out_tx: self.event_out_tx.clone(), }; - livekit_runtime::spawn(task.run()); - self.descriptors.insert(info.handle, Descriptor::Active(state_tx.clone())); + let join_handle = livekit_runtime::spawn(task.run()); + self.descriptors.insert(info.handle, Descriptor::Active { state_tx: state_tx.clone(), join_handle }); let inner = LocalTrackInner { frame_tx, state_tx }; LocalDataTrack::new(info, inner) @@ -295,7 +298,7 @@ impl ManagerTask { let Some(descriptor) = self.descriptors.remove(&event.handle) else { Err(anyhow!("No descriptor for track {}", event.handle))? }; - let Descriptor::Active(state_tx) = descriptor else { + let Descriptor::Active { state_tx, .. } = descriptor else { Err(anyhow!("Cannot unpublish pending track {}", event.handle))? }; if !state_tx.borrow().is_published() { @@ -308,16 +311,17 @@ impl ManagerTask { } /// Performs cleanup before the task ends. - fn shutdown(self) { + async fn shutdown(self) { for (_, descriptor) in self.descriptors { match descriptor { Descriptor::Pending(result_tx) => { _ = result_tx.send(Err(PublishError::Disconnected)) } - Descriptor::Active(state_tx) => { + Descriptor::Active { state_tx, join_handle } => { _ = state_tx.send(LocalTrackState::Unpublished { initiator: UnpublishInitiator::Shutdown, - }) + }); + join_handle.await; } } } From 4e7726141d941cf6d9ee412c0f77142c7100e23a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:02:18 +1100 Subject: [PATCH 098/232] Address todos, unit test --- livekit-datatrack/src/remote/manager.rs | 161 +++++++++++++++++------- 1 file changed, 119 insertions(+), 42 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 80d2d0157..535c07c37 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -25,6 +25,7 @@ use bytes::Bytes; use from_variants::FromVariants; use std::{ collections::{HashMap, HashSet}, + mem, sync::Arc, }; use tokio::sync::{broadcast, mpsc, oneshot, watch}; @@ -134,6 +135,7 @@ impl Manager { /// Sends an input event to the manager's task to be processed. pub fn send(&self, event: InputEvent) -> Result<(), InternalError> { + // TODO: try_send for data Ok(self.event_in_tx.try_send(event).context("Failed to send input event")?) } @@ -148,20 +150,20 @@ impl Manager { struct Descriptor { info: Arc, state_tx: watch::Sender, - state: DescriptorState + state: DescriptorState, } #[derive(Debug)] enum DescriptorState { Available, - Pending { + PendingSubscriberHandle { result_txs: Vec, SubscribeError>>>, }, Subscribed { packet_tx: mpsc::Sender, frame_tx: broadcast::Sender, - join_handle: livekit_runtime::JoinHandle<()> + join_handle: livekit_runtime::JoinHandle<()>, }, } @@ -181,8 +183,10 @@ impl ManagerTask { pub async fn run(mut self) { while let Some(event) = self.event_in_rx.recv().await { match event { - InputEvent::PublicationsUpdated(event) => self.handle_publications_updated(event), - InputEvent::Subscribe(event) => self.handle_subscribe(event), + InputEvent::PublicationsUpdated(event) => { + self.handle_publications_updated(event).await + } + InputEvent::Subscribe(event) => self.handle_subscribe(event).await, InputEvent::SubscriberHandles(event) => self.handle_subscriber_handles(event), InputEvent::PacketReceived(bytes) => self.handle_packet_received(bytes), InputEvent::Shutdown => break, @@ -191,7 +195,7 @@ impl ManagerTask { self.shutdown().await; } - fn handle_publications_updated(&mut self, event: PublicationsUpdatedEvent) { + async fn handle_publications_updated(&mut self, event: PublicationsUpdatedEvent) { let mut sids_in_update = HashSet::new(); // Detect published track @@ -201,7 +205,7 @@ impl ManagerTask { if self.descriptors.contains_key(&info.sid) { continue; } - self.handle_track_published(publisher_identity.clone(), info); + self.handle_track_published(publisher_identity.clone(), info).await; } } @@ -213,7 +217,7 @@ impl ManagerTask { } } - fn handle_track_published(&mut self, publisher_identity: String, info: DataTrackInfo) { + async fn handle_track_published(&mut self, publisher_identity: String, info: DataTrackInfo) { if self.descriptors.contains_key(&info.sid) { log::error!("Existing descriptor for track {}", info.sid); return; @@ -231,7 +235,7 @@ impl ManagerTask { publisher_identity, }; let track = RemoteDataTrack::new(info, inner); - _ = self.event_out_tx.send(track.into()); + _ = self.event_out_tx.send(track.into()).await; } fn handle_track_unpublished(&mut self, track_sid: String) { @@ -244,7 +248,7 @@ impl ManagerTask { // TODO: this should end the track task } - fn handle_subscribe(&mut self, event: SubscribeEvent) { + async fn handle_subscribe(&mut self, event: SubscribeEvent) { let Some(descriptor) = self.descriptors.get_mut(event.track_sid.as_str()) else { let error = SubscribeError::Internal(anyhow!("Cannot subscribe to unknown track").into()); @@ -257,11 +261,12 @@ impl ManagerTask { track_sid: event.track_sid.to_string(), subscribe: true, }; - _ = self.event_out_tx.send(update_event.into()); - descriptor.state = DescriptorState::Pending { result_txs: vec![event.result_tx] }; + _ = self.event_out_tx.send(update_event.into()).await; + descriptor.state = + DescriptorState::PendingSubscriberHandle { result_txs: vec![event.result_tx] }; // TODO: schedule timeout internally } - DescriptorState::Pending { result_txs } => { + DescriptorState::PendingSubscriberHandle { result_txs } => { result_txs.push(event.result_tx); } DescriptorState::Subscribed { frame_tx, .. } => { @@ -282,34 +287,37 @@ impl ManagerTask { log::warn!("Unknown track: {}", sid); return; }; - match &mut descriptor.state { - DescriptorState::Available => log::warn!("No subscription"), - DescriptorState::Pending { result_txs } => { - let (packet_tx, packet_rx) = mpsc::channel(4); // TODO: tune - let (frame_tx, frame_rx) = broadcast::channel(4); - - let track_task = RemoteTrackTask { - depacketizer: Depacketizer::new(), - decryption: self.decryption.clone(), - info: descriptor.info.clone(), - state_rx: descriptor.state_tx.subscribe(), - packet_rx, - frame_tx: frame_tx.clone(), - event_out_tx: self.event_out_tx.downgrade(), - }; - let join_handle = livekit_runtime::spawn(track_task.run()); - - descriptor.state = DescriptorState::Subscribed { packet_tx, frame_tx, join_handle }; - self.sub_handles.insert(handle, sid); - - // TODO: send completion - // for result_tx in result_txs { - // result_tx.send(Ok(frame_rx.resubscribe())); - // } + let result_txs = match &mut descriptor.state { + DescriptorState::Available => { + log::warn!("No subscription"); + return; } DescriptorState::Subscribed { .. } => { log::warn!("Handle reassignment not implemented"); + return; } + DescriptorState::PendingSubscriberHandle { result_txs } => mem::take(result_txs), + }; + + let (packet_tx, packet_rx) = mpsc::channel(4); // TODO: tune + let (frame_tx, frame_rx) = broadcast::channel(4); + + let track_task = RemoteTrackTask { + depacketizer: Depacketizer::new(), + decryption: self.decryption.clone(), + info: descriptor.info.clone(), + state_rx: descriptor.state_tx.subscribe(), + packet_rx, + frame_tx: frame_tx.clone(), + event_out_tx: self.event_out_tx.downgrade(), + }; + let join_handle = livekit_runtime::spawn(track_task.run()); + + descriptor.state = DescriptorState::Subscribed { packet_tx, frame_tx, join_handle }; + self.sub_handles.insert(handle, sid); + + for result_tx in result_txs { + _ = result_tx.send(Ok(frame_rx.resubscribe())); } } @@ -342,14 +350,12 @@ impl ManagerTask { _ = descriptor.state_tx.send(TrackState::Unpublished); match descriptor.state { DescriptorState::Available { .. } => {} - DescriptorState::Pending { result_txs } => { + DescriptorState::PendingSubscriberHandle { result_txs } => { for result_tx in result_txs { _ = result_tx.send(Err(SubscribeError::Disconnected)); } } - DescriptorState::Subscribed { join_handle, .. } => { - join_handle.await - } + DescriptorState::Subscribed { join_handle, .. } => join_handle.await, } } } @@ -358,7 +364,9 @@ impl ManagerTask { #[cfg(test)] mod tests { use super::*; - use std::time::Duration; + use futures_util::StreamExt; + use rstest::*; + use std::{collections::HashMap, time::Duration}; use tokio::time; #[tokio::test] @@ -371,4 +379,73 @@ mod tests { time::timeout(Duration::from_secs(1), join_handle).await.unwrap(); } + + #[rstest] + #[case("my_track", "some_identity")] + #[tokio::test] + async fn test_subscribe(#[case] name: String, #[case] publisher_identity: String) { + let options = ManagerOptions { decryption: None }; + let (manager, manager_task, mut output_events) = Manager::new(options); + livekit_runtime::spawn(manager_task.run()); + + // Simulate track published + let event = PublicationsUpdatedEvent { + tracks_by_participant: HashMap::from([( + publisher_identity.clone(), + vec![DataTrackInfo { + sid: "DTR_1234".try_into().unwrap(), + handle: 1024u32.try_into().unwrap(), + name: name.clone(), + uses_e2ee: false, + }], + )]), + }; + _ = manager.send(event.into()); + + let wait_for_track = async { + while let Some(event) = output_events.next().await { + match event { + OutputEvent::TrackAvailable(track) => return track, + _ => continue, + } + } + panic!("No track received"); + }; + + let track = wait_for_track.await; + assert!(track.is_published()); + assert_eq!(track.info().name, name); + assert_eq!(track.publisher_identity(), publisher_identity); + + let simulate_subscriber_handles = async { + while let Some(event) = output_events.next().await { + match event { + OutputEvent::SubscriptionUpdated(event) => { + assert!(event.subscribe); + assert_eq!(event.track_sid, "DTR_1234"); + time::sleep(Duration::from_millis(20)).await; + + // Simulate SFU reply + let event = SubscriberHandlesEvent { + mapping: HashMap::from([( + 64u32.try_into().unwrap(), + "DTR_1234".to_string(), + )]), + }; + _ = manager.send(event.into()); + } + _ => {} + } + } + }; + + time::timeout(Duration::from_secs(10), async { + tokio::select! { + _ = simulate_subscriber_handles => {} + _ = track.subscribe() => {} + } + }) + .await + .unwrap(); + } } From 141da9fc87c0695a2102c7b7cad6a0333ca0432b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:07:00 +1100 Subject: [PATCH 099/232] Type alias --- livekit-datatrack/src/remote/manager.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 535c07c37..b6cf62b40 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -71,14 +71,15 @@ pub struct SubscriberHandlesEvent { pub mapping: HashMap, } +type SubscribeResult = Result, SubscribeError>; + /// User requested to subscribe to a track. #[derive(Debug)] pub struct SubscribeEvent { /// Identifier of the track. pub(super) track_sid: String, /// Async completion channel. - pub(super) result_tx: - oneshot::Sender, SubscribeError>>, + pub(super) result_tx: oneshot::Sender, } /// User subscribed or unsubscribed to a track. @@ -157,8 +158,7 @@ struct Descriptor { enum DescriptorState { Available, PendingSubscriberHandle { - result_txs: - Vec, SubscribeError>>>, + result_txs: Vec>, }, Subscribed { packet_tx: mpsc::Sender, From 6d78cc8b01ef2ee6428a33326923f4495535366b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:31:22 +1100 Subject: [PATCH 100/232] Clean up imports --- livekit-datatrack/src/dtp/deserialize.rs | 7 +------ livekit-datatrack/src/dtp/packet.rs | 4 +--- livekit-datatrack/src/dtp/serialize.rs | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs index 20c69022c..eb462f509 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -12,12 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::dtp::time::Timestamp; - -use super::{ - packet::{consts::*, Dtp, E2ee, Header}, - track_handle::{TrackHandle, TrackHandleError}, -}; +use super::{consts::*, Dtp, E2ee, Header, Timestamp, TrackHandle, TrackHandleError}; use bytes::{Buf, Bytes}; use thiserror::Error; diff --git a/livekit-datatrack/src/dtp/packet.rs b/livekit-datatrack/src/dtp/packet.rs index ba5aa53ca..147195b00 100644 --- a/livekit-datatrack/src/dtp/packet.rs +++ b/livekit-datatrack/src/dtp/packet.rs @@ -12,9 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::dtp::time::Timestamp; - -use super::track_handle::TrackHandle; +use super::{TrackHandle, Timestamp}; use bytes::Bytes; use core::fmt; diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/dtp/serialize.rs index 103f0d6d2..0b3ef4a87 100644 --- a/livekit-datatrack/src/dtp/serialize.rs +++ b/livekit-datatrack/src/dtp/serialize.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::packet::{consts::*, Dtp, Header}; +use super::{consts::*, Dtp, Header}; use bytes::{BufMut, Bytes, BytesMut}; use thiserror::Error; From 93a9d571b8a30bc9346d4f34cd2223e6144e290c Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:34:31 +1100 Subject: [PATCH 101/232] Simplify naming --- livekit-datatrack/src/dtp/deserialize.rs | 6 ++-- .../src/dtp/{track_handle.rs => handle.rs} | 34 +++++++++---------- livekit-datatrack/src/dtp/mod.rs | 4 +-- livekit-datatrack/src/dtp/packet.rs | 4 +-- livekit-datatrack/src/local/manager.rs | 20 +++++------ livekit-datatrack/src/local/packetizer.rs | 10 +++--- livekit-datatrack/src/local/proto.rs | 8 ++--- livekit-datatrack/src/remote/manager.rs | 6 ++-- livekit-datatrack/src/remote/proto.rs | 6 ++-- livekit-datatrack/src/track.rs | 4 +-- livekit-datatrack/src/utils/handle_map.rs | 10 +++--- 11 files changed, 56 insertions(+), 56 deletions(-) rename livekit-datatrack/src/dtp/{track_handle.rs => handle.rs} (70%) diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs index eb462f509..7fe0d38c2 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{consts::*, Dtp, E2ee, Header, Timestamp, TrackHandle, TrackHandleError}; +use super::{consts::*, Dtp, E2ee, Header, Timestamp, Handle, HandleError}; use bytes::{Buf, Bytes}; use thiserror::Error; @@ -28,7 +28,7 @@ pub enum DeserializeError { UnsupportedVersion(u8), #[error("invalid track handle: {0}")] - InvalidTrackHandle(#[from] TrackHandleError), + InvalidHandle(#[from] HandleError), #[error("extension with id {0} is malformed")] MalformedExt(u8), @@ -68,7 +68,7 @@ impl Header { let extension_words = raw.get_u8(); let ext_len = 4 * extension_words as usize; - let track_handle: TrackHandle = raw.get_u16().try_into()?; + let track_handle: Handle = raw.get_u16().try_into()?; let sequence = raw.get_u16(); let frame_number = raw.get_u16(); let timestamp = Timestamp::from_ticks(raw.get_u32()); diff --git a/livekit-datatrack/src/dtp/track_handle.rs b/livekit-datatrack/src/dtp/handle.rs similarity index 70% rename from livekit-datatrack/src/dtp/track_handle.rs rename to livekit-datatrack/src/dtp/handle.rs index e162e5b78..07e66ad7d 100644 --- a/livekit-datatrack/src/dtp/track_handle.rs +++ b/livekit-datatrack/src/dtp/handle.rs @@ -17,10 +17,10 @@ use thiserror::Error; /// Handle identifying a data track at the transport level. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct TrackHandle(u16); +pub struct Handle(u16); #[derive(Debug, Error)] -pub enum TrackHandleError { +pub enum HandleError { #[error("{0:#X} is reserved")] Reserved(u16), @@ -28,39 +28,39 @@ pub enum TrackHandleError { TooLarge, } -impl TryFrom for TrackHandle { - type Error = TrackHandleError; +impl TryFrom for Handle { + type Error = HandleError; fn try_from(value: u16) -> Result { if value == 0 { - Err(TrackHandleError::Reserved(value))? + Err(HandleError::Reserved(value))? } Ok(Self(value)) } } -impl TryFrom for TrackHandle { - type Error = TrackHandleError; +impl TryFrom for Handle { + type Error = HandleError; fn try_from(value: u32) -> Result { - let value: u16 = value.try_into().map_err(|_| TrackHandleError::TooLarge)?; + let value: u16 = value.try_into().map_err(|_| HandleError::TooLarge)?; value.try_into() } } -impl From for u16 { - fn from(handle: TrackHandle) -> Self { +impl From for u16 { + fn from(handle: Handle) -> Self { handle.0 } } -impl From for u32 { - fn from(handle: TrackHandle) -> Self { +impl From for u32 { + fn from(handle: Handle) -> Self { handle.0 as u32 } } -impl Display for TrackHandle { +impl Display for Handle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{0:#X}", self.0) } @@ -68,15 +68,15 @@ impl Display for TrackHandle { /// Utility for allocating unique track handles to use for publishing tracks. #[derive(Debug, Default)] -pub struct TrackHandleAllocator { +pub struct HandleAllocator { /// Next handle value. value: u16, } -impl TrackHandleAllocator { +impl HandleAllocator { /// Returns a unique track handle for the next publication, if one can be obtained. - pub fn get(&mut self) -> Option { + pub fn get(&mut self) -> Option { let value = self.value.checked_add(1)?; - TrackHandle(value).into() + Handle(value).into() } } diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index 65e87efad..35557c828 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -16,10 +16,10 @@ mod deserialize; mod packet; mod serialize; mod time; -mod track_handle; +mod handle; pub use deserialize::*; pub use packet::*; pub use time::*; pub use serialize::*; -pub use track_handle::*; +pub use handle::*; diff --git a/livekit-datatrack/src/dtp/packet.rs b/livekit-datatrack/src/dtp/packet.rs index 147195b00..9d492307b 100644 --- a/livekit-datatrack/src/dtp/packet.rs +++ b/livekit-datatrack/src/dtp/packet.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{TrackHandle, Timestamp}; +use super::{Handle, Timestamp}; use bytes::Bytes; use core::fmt; @@ -25,7 +25,7 @@ pub struct Dtp { #[derive(Debug, Clone)] pub struct Header { pub is_final: bool, - pub track_handle: TrackHandle, + pub track_handle: Handle, pub sequence: u16, pub frame_number: u16, pub timestamp: Timestamp<90_000>, diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 32324ac87..a97deee0e 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -15,7 +15,7 @@ use super::{packetizer::Packetizer, pipeline::LocalTrackTask, LocalTrackInner}; use crate::{ api::{DataTrackInfo, DataTrackOptions, InternalError, PublishError}, - dtp::{self, TrackHandle}, + dtp::{self, Handle}, e2ee::EncryptionProvider, local::LocalDataTrack, }; @@ -51,7 +51,7 @@ pub enum OutputEvent { #[derive(Debug)] pub struct PublishResultEvent { /// Publisher handle of the track. - pub handle: TrackHandle, + pub handle: Handle, /// Outcome of the publish request. pub result: Result, } @@ -61,13 +61,13 @@ pub struct PublishResultEvent { #[derive(Debug)] pub struct UnpublishEvent { /// Publisher handle of the track that was unpublished. - pub handle: TrackHandle, + pub handle: Handle, } /// Local participant requested to publish a track. #[derive(Debug)] pub struct PublishRequestEvent { - pub handle: TrackHandle, + pub handle: Handle, pub name: String, pub uses_e2ee: bool, } @@ -80,7 +80,7 @@ pub struct PublishRequestEvent { #[derive(Debug)] pub struct UnpublishRequestEvent { /// Publisher handle of the track to unpublish. - pub handle: TrackHandle, + pub handle: Handle, } /// Request to publish a data track. @@ -96,7 +96,7 @@ pub struct PublishEvent { #[derive(Debug)] pub struct PublishTimeoutEvent { /// Publisher handle of the pending publication. - handle: TrackHandle, + handle: Handle, } #[derive(Debug, Clone, Copy)] @@ -140,7 +140,7 @@ impl Manager { event_in_tx: event_in_tx.downgrade(), event_in_rx, event_out_tx, - handle_allocator: dtp::TrackHandleAllocator::default(), + handle_allocator: dtp::HandleAllocator::default(), descriptors: HashMap::new(), }; @@ -197,8 +197,8 @@ pub struct ManagerTask { event_in_tx: mpsc::WeakSender, event_in_rx: mpsc::Receiver, event_out_tx: mpsc::Sender, - handle_allocator: dtp::TrackHandleAllocator, - descriptors: HashMap, + handle_allocator: dtp::HandleAllocator, + descriptors: HashMap, } impl ManagerTask { @@ -246,7 +246,7 @@ impl ManagerTask { Ok(()) } - fn schedule_publish_timeout(&self, handle: TrackHandle) { + fn schedule_publish_timeout(&self, handle: Handle) { let event_in_tx = self.event_in_tx.clone(); let emit_timeout = async move { time::sleep(Self::PUBLISH_TIMEOUT).await; diff --git a/livekit-datatrack/src/local/packetizer.rs b/livekit-datatrack/src/local/packetizer.rs index 5aa7672c6..25f1a0847 100644 --- a/livekit-datatrack/src/local/packetizer.rs +++ b/livekit-datatrack/src/local/packetizer.rs @@ -15,7 +15,7 @@ use crate::{ dtp::{ Clock, Timestamp, - Dtp, E2ee, Header, TrackHandle, + Dtp, E2ee, Header, Handle, }, utils::{BytesChunkExt, Counter}, }; @@ -25,7 +25,7 @@ use thiserror::Error; /// Converts application-level frames into packets for transport. #[derive(Debug)] pub struct Packetizer { - track_handle: TrackHandle, + handle: Handle, mtu_size: usize, sequence: Counter, frame_number: Counter, @@ -47,9 +47,9 @@ pub enum PacketizerError { impl Packetizer { /// Creates a new packetizer. - pub fn new(track_handle: TrackHandle, mtu_size: usize) -> Self { + pub fn new(track_handle: Handle, mtu_size: usize) -> Self { Self { - track_handle, + handle: track_handle, mtu_size, sequence: Default::default(), frame_number: Default::default(), @@ -62,7 +62,7 @@ impl Packetizer { // TODO: consider using default let header = Header { is_final: false, - track_handle: self.track_handle, + track_handle: self.handle, sequence: 0, frame_number: self.frame_number.get_then_increment(), timestamp: self.clock.now(), diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index 97deed41a..245dbcc6e 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -15,7 +15,7 @@ use super::manager::{PublishRequestEvent, UnpublishEvent, UnpublishRequestEvent}; use crate::{ api::{DataTrackInfo, InternalError, PublishError}, - dtp::TrackHandle, + dtp::Handle, local::manager::PublishResultEvent, }; use anyhow::{anyhow, Context}; @@ -53,7 +53,7 @@ impl TryFrom for UnpublishEvent { type Error = InternalError; fn try_from(msg: proto::UnpublishDataTrackResponse) -> Result { - let handle: TrackHandle = + let handle: Handle = msg.info.context("Missing info")?.pub_handle.try_into().map_err(anyhow::Error::from)?; Ok(Self { handle }) } @@ -63,7 +63,7 @@ impl TryFrom for DataTrackInfo { type Error = InternalError; fn try_from(msg: proto::DataTrackInfo) -> Result { - let handle: TrackHandle = msg.pub_handle.try_into().map_err(anyhow::Error::from)?; + let handle: Handle = msg.pub_handle.try_into().map_err(anyhow::Error::from)?; let uses_e2ee = match msg.encryption() { proto::encryption::Type::None => false, proto::encryption::Type::Gcm => true, @@ -79,7 +79,7 @@ pub fn publish_result_from_request_response( use proto::request_response::{Reason, Request}; let Some(request) = &msg.request else { return None }; let Request::PublishDataTrack(request) = request else { return None }; - let Ok(handle) = TryInto::::try_into(request.pub_handle) else { return None }; + let Ok(handle) = TryInto::::try_into(request.pub_handle) else { return None }; let error = match msg.reason() { // If new error reasons are introduced in the future, consider adding them // to the public error enum if they are useful to the user. diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index b6cf62b40..5bae4a110 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -15,7 +15,7 @@ use super::{depacketizer::Depacketizer, RemoteDataTrack, RemoteTrackInner}; use crate::{ api::{DataTrackFrame, DataTrackInfo, InternalError, SubscribeError}, - dtp::{Dtp, TrackHandle}, + dtp::{Dtp, Handle}, e2ee::DecryptionProvider, remote::pipeline::RemoteTrackTask, utils::HandleMap, @@ -68,7 +68,7 @@ pub struct PublicationsUpdatedEvent { pub struct SubscriberHandlesEvent { /// Mapping between track handles attached to incoming packets to the /// track SIDs they belong to. - pub mapping: HashMap, + pub mapping: HashMap, } type SubscribeResult = Result, SubscribeError>; @@ -282,7 +282,7 @@ impl ManagerTask { } } - fn register_subscriber_handle(&mut self, handle: TrackHandle, sid: String) { + fn register_subscriber_handle(&mut self, handle: Handle, sid: String) { let Some(descriptor) = self.descriptors.get_mut(&sid) else { log::warn!("Unknown track: {}", sid); return; diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index 44ce04ac9..7627f7d9f 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -15,7 +15,7 @@ use super::manager::{PublicationsUpdatedEvent, SubscriberHandlesEvent, SubscriptionUpdatedEvent}; use crate::{ api::{DataTrackInfo, InternalError}, - dtp::TrackHandle, + dtp::Handle, }; use livekit_protocol::{self as proto, ParticipantInfo}; use std::{collections::HashMap, mem}; @@ -30,10 +30,10 @@ impl TryFrom for SubscriberHandlesEvent { .sub_handles .into_iter() .map(|(handle, info)| -> Result<_, InternalError> { - let handle = TrackHandle::try_from(handle).map_err(anyhow::Error::from)?; + let handle = Handle::try_from(handle).map_err(anyhow::Error::from)?; Ok((handle, info.track_sid)) }) - .collect::, _>>()?; + .collect::, _>>()?; Ok(SubscriberHandlesEvent { mapping }) } } diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index ec227a030..ca761c161 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::dtp::TrackHandle; +use crate::dtp::Handle; use from_variants::FromVariants; use std::{marker::PhantomData, sync::Arc}; @@ -20,7 +20,7 @@ use std::{marker::PhantomData, sync::Arc}; #[derive(Debug, Clone)] pub struct DataTrackInfo { pub(crate) sid: String, // TODO: use shared ID type - pub(crate) handle: TrackHandle, // TODO: consider removing (protocol level detail) + pub(crate) handle: Handle, // TODO: consider removing (protocol level detail) pub(crate) name: String, pub(crate) uses_e2ee: bool, } diff --git a/livekit-datatrack/src/utils/handle_map.rs b/livekit-datatrack/src/utils/handle_map.rs index 8dfbd6467..9f520391c 100644 --- a/livekit-datatrack/src/utils/handle_map.rs +++ b/livekit-datatrack/src/utils/handle_map.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::dtp::TrackHandle; +use crate::dtp::Handle; use std::collections::HashMap; /// Map between track handle and SID. @@ -21,8 +21,8 @@ use std::collections::HashMap; /// #[derive(Debug, Default)] pub struct HandleMap { - sid_to_handle: HashMap, - handle_to_sid: HashMap, + sid_to_handle: HashMap, + handle_to_sid: HashMap, } impl HandleMap { @@ -31,7 +31,7 @@ impl HandleMap { /// Returns a Boolean indicating whether the entry was inserted. /// Insertion will fail if the mapping already exists in either direction. /// - pub fn insert(&mut self, handle: TrackHandle, sid: String) -> bool { + pub fn insert(&mut self, handle: Handle, sid: String) -> bool { if self.sid_to_handle.contains_key(&sid) || self.handle_to_sid.contains_key(&handle) { return false; } @@ -41,7 +41,7 @@ impl HandleMap { } /// Get the SID associated with the given handle. - pub fn get_sid(&self, handle: TrackHandle) -> Option<&String> { + pub fn get_sid(&self, handle: Handle) -> Option<&String> { self.handle_to_sid.get(&handle) } From 3b0508a8bf15ea63ba087792b864d13c1f2ce0c4 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:38:27 +1100 Subject: [PATCH 102/232] Doc --- 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 7b3b9fddd..5877749b0 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -27,7 +27,7 @@ mod frame; /// End-to-end encryption. mod e2ee; -/// Data track packet serialization & packetization. +/// Data track packet format. mod dtp; /// Common utilities. From 4012fd346d1057a6a3cd357ced26b0db67d63a4b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:09:16 +1100 Subject: [PATCH 103/232] Sid wrapper type --- livekit-datatrack/src/local/manager.rs | 2 +- livekit-datatrack/src/local/proto.rs | 7 ++-- livekit-datatrack/src/remote/manager.rs | 30 ++++++------- livekit-datatrack/src/remote/mod.rs | 2 +- livekit-datatrack/src/remote/proto.rs | 23 ++++++---- livekit-datatrack/src/track.rs | 51 +++++++++++++++++++++-- livekit-datatrack/src/utils/handle_map.rs | 12 +++--- 7 files changed, 90 insertions(+), 37 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index a97deee0e..f30e7969c 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -372,7 +372,7 @@ mod tests { OutputEvent::PublishRequest(event) => { // SFU accepts publication let info = DataTrackInfo { - sid: "DTR_1234".into(), + sid: "DTR_1234".to_string().try_into().unwrap(), handle: 1u32.try_into().unwrap(), name: event.name, uses_e2ee: event.uses_e2ee, diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index 245dbcc6e..feece3f6a 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -14,7 +14,7 @@ use super::manager::{PublishRequestEvent, UnpublishEvent, UnpublishRequestEvent}; use crate::{ - api::{DataTrackInfo, InternalError, PublishError}, + api::{DataTrackInfo, DataTrackSid, InternalError, PublishError}, dtp::Handle, local::manager::PublishResultEvent, }; @@ -69,7 +69,8 @@ impl TryFrom for DataTrackInfo { proto::encryption::Type::Gcm => true, other => Err(anyhow!("Unsupported E2EE type: {:?}", other))?, }; - Ok(Self { handle, sid: msg.sid, name: msg.name, uses_e2ee }) + let sid: DataTrackSid = msg.sid.try_into().map_err(anyhow::Error::from)?; + Ok(Self { handle, sid, name: msg.name, uses_e2ee }) } } @@ -140,7 +141,7 @@ mod tests { let info = event.result.expect("Expected ok result"); assert_eq!(info.handle, 1u32.try_into().unwrap()); - assert_eq!(info.sid, "DTR_1234"); + assert_eq!(info.sid, "DTR_1234".to_string().try_into().unwrap()); assert_eq!(info.name, "track"); assert!(info.uses_e2ee); } diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 5bae4a110..18cb47e39 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -14,7 +14,7 @@ use super::{depacketizer::Depacketizer, RemoteDataTrack, RemoteTrackInner}; use crate::{ - api::{DataTrackFrame, DataTrackInfo, InternalError, SubscribeError}, + api::{DataTrackFrame, DataTrackInfo, DataTrackSid, InternalError, SubscribeError}, dtp::{Dtp, Handle}, e2ee::DecryptionProvider, remote::pipeline::RemoteTrackTask, @@ -68,7 +68,7 @@ pub struct PublicationsUpdatedEvent { pub struct SubscriberHandlesEvent { /// Mapping between track handles attached to incoming packets to the /// track SIDs they belong to. - pub mapping: HashMap, + pub mapping: HashMap, } type SubscribeResult = Result, SubscribeError>; @@ -77,7 +77,7 @@ type SubscribeResult = Result, SubscribeErro #[derive(Debug)] pub struct SubscribeEvent { /// Identifier of the track. - pub(super) track_sid: String, + pub(super) sid: DataTrackSid, /// Async completion channel. pub(super) result_tx: oneshot::Sender, } @@ -86,7 +86,7 @@ pub struct SubscribeEvent { #[derive(Debug)] pub struct SubscriptionUpdatedEvent { /// Identifier of the affected track. - pub track_sid: String, + pub sid: DataTrackSid, /// Whether to subscribe or unsubscribe. pub subscribe: bool, } @@ -174,7 +174,7 @@ pub struct ManagerTask { event_out_tx: mpsc::Sender, /// Mapping between track SID and descriptor. - descriptors: HashMap, + descriptors: HashMap, /// Bidirectional mapping between track SID and subscriber handle. sub_handles: HandleMap, } @@ -238,10 +238,10 @@ impl ManagerTask { _ = self.event_out_tx.send(track.into()).await; } - fn handle_track_unpublished(&mut self, track_sid: String) { - self.sub_handles.remove(&track_sid); - let Some(descriptor) = self.descriptors.remove(&track_sid) else { - log::error!("Unknown track {}", track_sid); + fn handle_track_unpublished(&mut self, sid: DataTrackSid) { + self.sub_handles.remove(&sid); + let Some(descriptor) = self.descriptors.remove(&sid) else { + log::error!("Unknown track {}", sid); return; }; _ = descriptor.state_tx.send(TrackState::Unpublished); @@ -249,7 +249,7 @@ impl ManagerTask { } async fn handle_subscribe(&mut self, event: SubscribeEvent) { - let Some(descriptor) = self.descriptors.get_mut(event.track_sid.as_str()) else { + let Some(descriptor) = self.descriptors.get_mut(&event.sid) else { let error = SubscribeError::Internal(anyhow!("Cannot subscribe to unknown track").into()); _ = event.result_tx.send(Err(error)); @@ -258,7 +258,7 @@ impl ManagerTask { match &mut descriptor.state { DescriptorState::Available => { let update_event = SubscriptionUpdatedEvent { - track_sid: event.track_sid.to_string(), + sid: event.sid.clone(), subscribe: true, }; _ = self.event_out_tx.send(update_event.into()).await; @@ -282,7 +282,7 @@ impl ManagerTask { } } - fn register_subscriber_handle(&mut self, handle: Handle, sid: String) { + fn register_subscriber_handle(&mut self, handle: Handle, sid: DataTrackSid) { let Some(descriptor) = self.descriptors.get_mut(&sid) else { log::warn!("Unknown track: {}", sid); return; @@ -393,7 +393,7 @@ mod tests { tracks_by_participant: HashMap::from([( publisher_identity.clone(), vec![DataTrackInfo { - sid: "DTR_1234".try_into().unwrap(), + sid: "DTR_1234".to_string().try_into().unwrap(), handle: 1024u32.try_into().unwrap(), name: name.clone(), uses_e2ee: false, @@ -422,14 +422,14 @@ mod tests { match event { OutputEvent::SubscriptionUpdated(event) => { assert!(event.subscribe); - assert_eq!(event.track_sid, "DTR_1234"); + assert_eq!(event.sid, "DTR_1234".to_string().try_into().unwrap()); time::sleep(Duration::from_millis(20)).await; // Simulate SFU reply let event = SubscriberHandlesEvent { mapping: HashMap::from([( 64u32.try_into().unwrap(), - "DTR_1234".to_string(), + "DTR_1234".to_string().try_into().unwrap(), )]), }; _ = manager.send(event.into()); diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 0ed567a7f..cfa86e13d 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -52,7 +52,7 @@ impl DataTrack { /// Subscribe to the data track to receive frames. pub async fn subscribe(&self) -> Result, SubscribeError> { let (result_tx, result_rx) = oneshot::channel(); - let subscribe_event = SubscribeEvent { track_sid: self.info.sid.clone(), result_tx }; + let subscribe_event = SubscribeEvent { sid: self.info.sid.clone(), result_tx }; self.inner() .event_in_tx .upgrade() diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index 7627f7d9f..b0d714ad4 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -14,7 +14,7 @@ use super::manager::{PublicationsUpdatedEvent, SubscriberHandlesEvent, SubscriptionUpdatedEvent}; use crate::{ - api::{DataTrackInfo, InternalError}, + api::{DataTrackInfo, DataTrackSid, InternalError}, dtp::Handle, }; use livekit_protocol::{self as proto, ParticipantInfo}; @@ -30,10 +30,11 @@ impl TryFrom for SubscriberHandlesEvent { .sub_handles .into_iter() .map(|(handle, info)| -> Result<_, InternalError> { - let handle = Handle::try_from(handle).map_err(anyhow::Error::from)?; - Ok((handle, info.track_sid)) + let handle: Handle = handle.try_into().map_err(anyhow::Error::from)?; + let sid: DataTrackSid = info.track_sid.try_into().map_err(anyhow::Error::from)?; + Ok((handle, sid)) }) - .collect::, _>>()?; + .collect::, _>>()?; Ok(SubscriberHandlesEvent { mapping }) } } @@ -84,7 +85,7 @@ fn extract_track_info(msg: &mut ParticipantInfo) -> Result, I impl From for proto::UpdateDataSubscription { fn from(event: SubscriptionUpdatedEvent) -> Self { let update = proto::update_data_subscription::Update { - track_sid: event.track_sid, + track_sid: event.sid.into(), subscribe: event.subscribe, options: Default::default(), // TODO: pass through options }; @@ -118,8 +119,14 @@ mod tests { proto::DataTrackSubscriberHandles { sub_handles: HashMap::from(sub_handles) }; let event: SubscriberHandlesEvent = subscriber_handles.try_into().unwrap(); - assert_eq!(event.mapping.get(&1u32.try_into().unwrap()).unwrap(), "DTR_1234"); - assert_eq!(event.mapping.get(&2u32.try_into().unwrap()).unwrap(), "DTR_4567"); + assert_eq!( + event.mapping.get(&1u32.try_into().unwrap()).unwrap(), + &"DTR_1234".to_string().try_into().unwrap() + ); + assert_eq!( + event.mapping.get(&2u32.try_into().unwrap()).unwrap(), + &"DTR_4567".to_string().try_into().unwrap() + ); } #[test] @@ -139,6 +146,6 @@ mod tests { let first = track_info.first().unwrap(); assert_eq!(first.handle, 1u32.try_into().unwrap()); assert_eq!(first.name, "track1"); - assert_eq!(first.sid, "DTR_1234"); + assert_eq!(first.sid, "DTR_1234".to_string().try_into().unwrap()); } } diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index ca761c161..3bd82077f 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -14,12 +14,13 @@ use crate::dtp::Handle; use from_variants::FromVariants; -use std::{marker::PhantomData, sync::Arc}; +use std::{fmt::Display, marker::PhantomData, ops::Deref, sync::Arc}; +use thiserror::Error; /// Information about a published data track. #[derive(Debug, Clone)] pub struct DataTrackInfo { - pub(crate) sid: String, // TODO: use shared ID type + pub(crate) sid: DataTrackSid, pub(crate) handle: Handle, // TODO: consider removing (protocol level detail) pub(crate) name: String, pub(crate) uses_e2ee: bool, @@ -27,7 +28,7 @@ pub struct DataTrackInfo { impl DataTrackInfo { /// Unique track identifier. - pub fn sid(&self) -> &str { + pub fn sid(&self) -> &DataTrackSid { &self.sid } /// Name of the track assigned when published. @@ -60,3 +61,47 @@ impl DataTrack { &self.info } } + +/// SFU-assigned identifier uniquely identifying a data track. +#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct DataTrackSid(String); + +#[derive(Debug, Error)] +#[error("Invalid data track SID")] +pub struct DataTrackSidError; + +impl DataTrackSid { + const PREFIX: &str = "DTR_"; +} + +impl TryFrom for DataTrackSid { + type Error = DataTrackSidError; + + fn try_from(raw_id: String) -> Result { + if raw_id.starts_with(Self::PREFIX) { + Ok(Self(raw_id)) + } else { + Err(DataTrackSidError) + } + } +} + +impl From for String { + fn from(id: DataTrackSid) -> Self { + id.0 + } +} + +impl Deref for DataTrackSid { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for DataTrackSid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/livekit-datatrack/src/utils/handle_map.rs b/livekit-datatrack/src/utils/handle_map.rs index 9f520391c..a009cbaaf 100644 --- a/livekit-datatrack/src/utils/handle_map.rs +++ b/livekit-datatrack/src/utils/handle_map.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::dtp::Handle; +use crate::{api::DataTrackSid, dtp::Handle}; use std::collections::HashMap; /// Map between track handle and SID. @@ -21,8 +21,8 @@ use std::collections::HashMap; /// #[derive(Debug, Default)] pub struct HandleMap { - sid_to_handle: HashMap, - handle_to_sid: HashMap, + sid_to_handle: HashMap, + handle_to_sid: HashMap, } impl HandleMap { @@ -31,7 +31,7 @@ impl HandleMap { /// Returns a Boolean indicating whether the entry was inserted. /// Insertion will fail if the mapping already exists in either direction. /// - pub fn insert(&mut self, handle: Handle, sid: String) -> bool { + pub fn insert(&mut self, handle: Handle, sid: DataTrackSid) -> bool { if self.sid_to_handle.contains_key(&sid) || self.handle_to_sid.contains_key(&handle) { return false; } @@ -41,12 +41,12 @@ impl HandleMap { } /// Get the SID associated with the given handle. - pub fn get_sid(&self, handle: Handle) -> Option<&String> { + pub fn get_sid(&self, handle: Handle) -> Option<&DataTrackSid> { self.handle_to_sid.get(&handle) } /// Remove the mapping with the given SID. - pub fn remove(&mut self, sid: &str) { + pub fn remove(&mut self, sid: &DataTrackSid) { let Some(handle) = self.sid_to_handle.remove(sid) else { return }; self.handle_to_sid.remove(&handle); } From 4654ccb44921c40309bd217f5330424f924359e3 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:18:48 +1100 Subject: [PATCH 104/232] Log instead of result --- livekit-datatrack/src/local/manager.rs | 41 ++++++++++++------------- livekit-datatrack/src/local/pipeline.rs | 28 +++++++++++------ 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index f30e7969c..777fb51c6 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -207,32 +207,31 @@ impl ManagerTask { if matches!(event, InputEvent::Shutdown) { break; } - let Err(err) = self.handle_event(event) else { continue }; - log::error!("Failed to handle input event: {}", err); + self.handle_event(event); } self.shutdown().await; } - fn handle_event(&mut self, event: InputEvent) -> Result<(), InternalError> { + fn handle_event(&mut self, event: InputEvent) { match event { InputEvent::Publish(event) => self.handle_publish(event), InputEvent::PublishResult(event) => self.handle_publish_result(event), InputEvent::Unpublish(event) => self.handle_unpublished(event), - _ => Ok(()), + _ => {} } } - fn handle_publish(&mut self, event: PublishEvent) -> Result<(), InternalError> { + fn handle_publish(&mut self, event: PublishEvent) { let Some(handle) = self.handle_allocator.get() else { _ = event.result_tx.send(Err(PublishError::LimitReached)); - return Ok(()); + return; }; if self.descriptors.contains_key(&handle) { _ = event.result_tx.send(Err(PublishError::Internal( anyhow!("Descriptor for handle already exists").into(), ))); - return Ok(()); + return; } self.descriptors.insert(handle, Descriptor::Pending(event.result_tx)); @@ -243,7 +242,6 @@ impl ManagerTask { }; _ = self.event_out_tx.try_send(publish_requested.into()); // TODO: check for error. self.schedule_publish_timeout(handle); - Ok(()) } fn schedule_publish_timeout(&self, handle: Handle) { @@ -257,20 +255,21 @@ impl ManagerTask { livekit_runtime::spawn(emit_timeout); } - fn handle_publish_result(&mut self, event: PublishResultEvent) -> Result<(), InternalError> { + fn handle_publish_result(&mut self, event: PublishResultEvent) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { - Err(anyhow!("No descriptor for {}", event.handle))? + log::warn!("No descriptor for {}", event.handle); + return }; let Descriptor::Pending(result_tx) = descriptor else { - Err(anyhow!("Track {} already active", event.handle))? + log::warn!("Track {} already active", event.handle); + return }; if result_tx.is_closed() { - return Ok(()); + return; } let result = event.result.map(|track_info| self.create_local_track(track_info)); _ = result_tx.send(result); - Ok(()) } fn create_local_track(&mut self, info: DataTrackInfo) -> LocalDataTrack { @@ -294,20 +293,20 @@ impl ManagerTask { LocalDataTrack::new(info, inner) } - fn handle_unpublished(&mut self, event: UnpublishEvent) -> Result<(), InternalError> { + fn handle_unpublished(&mut self, event: UnpublishEvent) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { - Err(anyhow!("No descriptor for track {}", event.handle))? + log::warn!("No descriptor for track {}", event.handle); + return }; let Descriptor::Active { state_tx, .. } = descriptor else { - Err(anyhow!("Cannot unpublish pending track {}", event.handle))? + log::warn!("Cannot unpublish pending track {}", event.handle); + return }; if !state_tx.borrow().is_published() { - return Ok(()); + return } - state_tx - .send(LocalTrackState::Unpublished { initiator: UnpublishInitiator::Sfu }) - .context("Failed to set state")?; - Ok(()) + _ = state_tx + .send(LocalTrackState::Unpublished { initiator: UnpublishInitiator::Sfu }); } /// Performs cleanup before the task ends. diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index b46e20f85..37ed47d5b 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -14,12 +14,11 @@ use super::packetizer::{Packetizer, PacketizerFrame}; use crate::{ - api::{DataTrackFrame, DataTrackInfo, InternalError}, + api::{DataTrackFrame, DataTrackInfo}, dtp, e2ee::EncryptionProvider, local::manager::{LocalTrackState, OutputEvent, UnpublishInitiator, UnpublishRequestEvent}, }; -use anyhow::Context; use std::sync::Arc; use tokio::sync::{mpsc, watch}; @@ -43,7 +42,7 @@ impl LocalTrackTask { state = *self.state_rx.borrow(); }, Some(frame) = self.frame_rx.recv() => { - _ = self.publish_frame(frame).inspect_err(|err| log::error!("{}", err)); + self.publish_frame(frame); }, else => break } @@ -54,12 +53,17 @@ impl LocalTrackTask { } } - fn publish_frame(&mut self, mut frame: DataTrackFrame) -> Result<(), InternalError> { + fn publish_frame(&mut self, mut frame: DataTrackFrame) { let mut e2ee: Option = None; if let Some(encryption) = &self.encryption { debug_assert!(self.info.uses_e2ee); - let encrypted_payload = - encryption.encrypt(frame.payload).context("Failed to encrypt frame")?; + let encrypted_payload = match encryption.encrypt(frame.payload) { + Ok(payload) => payload, + Err(err) => { + log::error!("Failed to encrypt frame: {}", err); + return; + } + }; e2ee = Some(dtp::E2ee { key_index: encrypted_payload.key_index, iv: encrypted_payload.iv, @@ -69,11 +73,17 @@ impl LocalTrackTask { let frame = PacketizerFrame { payload: frame.payload, e2ee, user_timestamp: frame.user_timestamp }; - let packets = self.packetizer.packetize(frame).context("Failed to packetize frame")?; + + let packets = match self.packetizer.packetize(frame) { + Ok(packets) => packets, + Err(err) => { + log::error!("Failed to packetize frame: {}", err); + return; + } + }; for packet in packets { let serialized = packet.serialize(); - self.event_out_tx.try_send(serialized.into()).context("Failed to send packet")?; + _ = self.event_out_tx.try_send(serialized.into()); } - Ok(()) } } From 63f943dd2b4934c1b3151eadb630717d0a17bf28 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 8 Jan 2026 13:19:33 +1100 Subject: [PATCH 105/232] Module organization --- livekit-datatrack/src/dtp/mod.rs | 93 ++++++++++++++++++++++-- livekit-datatrack/src/dtp/packet.rs | 99 -------------------------- livekit-datatrack/src/dtp/serialize.rs | 4 +- 3 files changed, 89 insertions(+), 107 deletions(-) delete mode 100644 livekit-datatrack/src/dtp/packet.rs diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index 35557c828..afefba9c8 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -12,14 +12,97 @@ // See the License for the specific language governing permissions and // limitations under the License. +use bytes::Bytes; +use core::fmt; + mod deserialize; -mod packet; +mod handle; mod serialize; mod time; -mod handle; pub use deserialize::*; -pub use packet::*; -pub use time::*; -pub use serialize::*; pub use handle::*; +pub use serialize::*; +pub use time::*; + +#[derive(Clone)] +pub struct Dtp { + pub header: Header, + pub payload: Bytes, +} + +#[derive(Debug, Clone)] +pub struct Header { + pub is_final: bool, + pub track_handle: Handle, + pub sequence: u16, + pub frame_number: u16, + pub timestamp: Timestamp<90_000>, + pub user_timestamp: Option, + pub e2ee: Option, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct E2ee { + pub key_index: u8, + pub iv: [u8; 12], +} + +impl Dtp { + /// Whether the packet is the final one in a frame. + pub fn is_final(&self) -> bool { + self.header.is_final + } + + /// Whether the packet's payload is encrypted. + pub fn is_encrypted(&self) -> bool { + self.header.e2ee.is_some() + } +} + +impl fmt::Debug for Dtp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Dtp") + .field("header", &self.header) + .field("payload_len", &self.payload.len()) + .finish() + } +} + +impl fmt::Debug for E2ee { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // For security, do not include fields in debug. + f.debug_struct("E2ee").finish() + } +} + +/// Constants used in sterilization and deserialization. +pub(crate) mod consts { + pub const SUPPORTED_VERSION: u8 = 0; + pub const BASE_HEADER_LEN: usize = 12; + + // Bitfield shifts and masks for header flags + pub const VERSION_SHIFT: u8 = 5; + pub const VERSION_MASK: u8 = 0x07; + pub const FINAL_FLAG_SHIFT: u8 = 4; + pub const FINAL_FLAG_MASK: u8 = 0x01; + + // Extension IDs + pub const EXT_ID_E2EE: u8 = 0x1; + pub const EXT_ID_USER_TIMESTAMP: u8 = 0x2; + pub const EXT_ID_INVALID: u8 = 0xF; + + // Extension lengths + pub const EXT_LEN_E2EE: usize = 13; + pub const EXT_LEN_USER_TIMESTAMP: usize = 8; + + // Extension markers + pub const EXT_MARKER_LEN: usize = 1; + pub const EXT_MARKER_E2EE: u8 = ext_marker(EXT_ID_E2EE, EXT_LEN_E2EE as u8); + pub const EXT_MARKER_USER_TIMESTAMP: u8 = + ext_marker(EXT_ID_USER_TIMESTAMP, EXT_LEN_USER_TIMESTAMP as u8); + + const fn ext_marker(id: u8, len: u8) -> u8 { + (id << 4) | (len - 1) + } +} diff --git a/livekit-datatrack/src/dtp/packet.rs b/livekit-datatrack/src/dtp/packet.rs deleted file mode 100644 index 9d492307b..000000000 --- a/livekit-datatrack/src/dtp/packet.rs +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2025 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 super::{Handle, Timestamp}; -use bytes::Bytes; -use core::fmt; - -#[derive(Clone)] -pub struct Dtp { - pub header: Header, - pub payload: Bytes, -} - -#[derive(Debug, Clone)] -pub struct Header { - pub is_final: bool, - pub track_handle: Handle, - pub sequence: u16, - pub frame_number: u16, - pub timestamp: Timestamp<90_000>, - pub user_timestamp: Option, - pub e2ee: Option, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct E2ee { - pub key_index: u8, - pub iv: [u8; 12], -} - -impl Dtp { - /// Whether the packet is the final one in a frame. - pub fn is_final(&self) -> bool { - self.header.is_final - } - - /// Whether the packet's payload is encrypted. - pub fn is_encrypted(&self) -> bool { - self.header.e2ee.is_some() - } -} - -impl fmt::Debug for Dtp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Dtp") - .field("header", &self.header) - .field("payload_len", &self.payload.len()) - .finish() - } -} - -impl fmt::Debug for E2ee { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // For security, do not include fields in debug. - f.debug_struct("E2ee").finish() - } -} - -/// Constants used in sterilization and deserialization. -pub(crate) mod consts { - pub const SUPPORTED_VERSION: u8 = 0; - pub const BASE_HEADER_LEN: usize = 12; - - // Bitfield shifts and masks for header flags - pub const VERSION_SHIFT: u8 = 5; - pub const VERSION_MASK: u8 = 0x07; - pub const FINAL_FLAG_SHIFT: u8 = 4; - pub const FINAL_FLAG_MASK: u8 = 0x01; - - // Extension IDs - pub const EXT_ID_E2EE: u8 = 0x1; - pub const EXT_ID_USER_TIMESTAMP: u8 = 0x2; - pub const EXT_ID_INVALID: u8 = 0xF; - - // Extension lengths - pub const EXT_LEN_E2EE: usize = 13; - pub const EXT_LEN_USER_TIMESTAMP: usize = 8; - - // Extension markers - pub const EXT_MARKER_LEN: usize = 1; - pub const EXT_MARKER_E2EE: u8 = ext_marker(EXT_ID_E2EE, EXT_LEN_E2EE as u8); - pub const EXT_MARKER_USER_TIMESTAMP: u8 = - ext_marker(EXT_ID_USER_TIMESTAMP, EXT_LEN_USER_TIMESTAMP as u8); - - const fn ext_marker(id: u8, len: u8) -> u8 { - (id << 4) | (len - 1) - } -} diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/dtp/serialize.rs index 0b3ef4a87..03516f35b 100644 --- a/livekit-datatrack/src/dtp/serialize.rs +++ b/livekit-datatrack/src/dtp/serialize.rs @@ -134,9 +134,7 @@ impl Header { #[cfg(test)] mod tests { - use crate::dtp::time::Timestamp; - - use super::{super::packet::E2ee, *}; + use crate::dtp::{Dtp, Header, E2ee, Timestamp}; use bytes::Buf; /// Constructed packet to use in tests. From b35ce0ccfedff29005ce26c27b1d7d5f640ce713 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:09:51 +1100 Subject: [PATCH 106/232] Dtp frame marker --- livekit-datatrack/src/dtp/deserialize.rs | 16 ++++++---- livekit-datatrack/src/dtp/mod.rs | 37 +++++++++++++++++------ livekit-datatrack/src/dtp/serialize.rs | 19 +++++++----- livekit-datatrack/src/local/packetizer.rs | 22 +++++++++----- 4 files changed, 65 insertions(+), 29 deletions(-) diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs index 7fe0d38c2..610fee0e8 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{consts::*, Dtp, E2ee, Header, Timestamp, Handle, HandleError}; +use super::{consts::*, Dtp, E2ee, FrameMarker, Handle, HandleError, Header, Timestamp}; use bytes::{Buf, Bytes}; use thiserror::Error; @@ -63,8 +63,12 @@ impl Header { if version > SUPPORTED_VERSION { Err(DeserializeError::UnsupportedVersion(version))? } - let is_final = (initial >> FINAL_FLAG_SHIFT & FINAL_FLAG_MASK) > 0; - + let frame_marker = match initial >> FRAME_MARKER_SHIFT & FRAME_MARKER_MASK { + FRAME_MARKER_START => FrameMarker::Start, + FRAME_MARKER_FINAL => FrameMarker::Final, + FRAME_MARKER_SINGLE => FrameMarker::Single, + _ => FrameMarker::Inter, + }; let extension_words = raw.get_u8(); let ext_len = 4 * extension_words as usize; @@ -80,7 +84,7 @@ impl Header { let extensions = Extensions::parse(ext_block)?; let header = Header { - is_final, + frame_marker, track_handle, sequence, frame_number, @@ -176,7 +180,7 @@ mod tests { #[test] fn test_base_header() { let mut raw = BytesMut::new(); - raw.put_u8(0x10); // Version 0, final flag set + raw.put_u8(0x8); // Version 0, start flag set raw.put_u8(0x0); // No extension words raw.put_slice(&[0x88, 0x11]); // Track ID raw.put_slice(&[0x44, 0x22]); // Sequence @@ -184,7 +188,7 @@ mod tests { raw.put_slice(&[0x44, 0x22, 0x11, 0x88]); // Timestamp let dtp = Dtp::deserialize(raw.freeze()).unwrap(); - assert_eq!(dtp.header.is_final, true); + assert_eq!(dtp.header.frame_marker, FrameMarker::Final); assert_eq!(dtp.header.track_handle, 0x8811u32.try_into().unwrap()); assert_eq!(dtp.header.sequence, 0x4422); assert_eq!(dtp.header.frame_number, 0x4411); diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index afefba9c8..e813f0494 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -33,7 +33,7 @@ pub struct Dtp { #[derive(Debug, Clone)] pub struct Header { - pub is_final: bool, + pub frame_marker: FrameMarker, pub track_handle: Handle, pub sequence: u16, pub frame_number: u16, @@ -42,6 +42,24 @@ pub struct Header { pub e2ee: Option, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameMarker { + /// Packet is within a frame. + /// + /// This corresponds to neither the start nor final flag being set. + /// + Inter, + /// Packet is the last in a frame. + Final, + /// Packet is the first in a frame. + Start, + /// Packet is the only one in the frame. + /// + /// This corresponds to both the start and final flag being set. + /// + Single +} + #[derive(Clone, Copy, PartialEq, Eq)] pub struct E2ee { pub key_index: u8, @@ -49,11 +67,6 @@ pub struct E2ee { } impl Dtp { - /// Whether the packet is the final one in a frame. - pub fn is_final(&self) -> bool { - self.header.is_final - } - /// Whether the packet's payload is encrypted. pub fn is_encrypted(&self) -> bool { self.header.e2ee.is_some() @@ -76,7 +89,7 @@ impl fmt::Debug for E2ee { } } -/// Constants used in sterilization and deserialization. +/// Constants used for serialization and deserialization. pub(crate) mod consts { pub const SUPPORTED_VERSION: u8 = 0; pub const BASE_HEADER_LEN: usize = 12; @@ -84,8 +97,14 @@ pub(crate) mod consts { // Bitfield shifts and masks for header flags pub const VERSION_SHIFT: u8 = 5; pub const VERSION_MASK: u8 = 0x07; - pub const FINAL_FLAG_SHIFT: u8 = 4; - pub const FINAL_FLAG_MASK: u8 = 0x01; + + pub const FRAME_MARKER_SHIFT: u8 = 3; + pub const FRAME_MARKER_MASK: u8 = 0x3; + + pub const FRAME_MARKER_START: u8 = 0x2; + pub const FRAME_MARKER_FINAL: u8 = 0x1; + pub const FRAME_MARKER_INTER: u8 = 0x0; + pub const FRAME_MARKER_SINGLE: u8 = 0x3; // Extension IDs pub const EXT_ID_E2EE: u8 = 0x1; diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/dtp/serialize.rs index 03516f35b..aa1adfa5a 100644 --- a/livekit-datatrack/src/dtp/serialize.rs +++ b/livekit-datatrack/src/dtp/serialize.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{consts::*, Dtp, Header}; +use super::{consts::*, Dtp, FrameMarker, Header}; use bytes::{BufMut, Bytes, BytesMut}; use thiserror::Error; @@ -67,9 +67,14 @@ impl Header { } let mut initial = SUPPORTED_VERSION << VERSION_SHIFT; - if self.is_final { - initial |= 1 << FINAL_FLAG_SHIFT; - } + + let frame_marker = match self.frame_marker { + FrameMarker::Inter => FRAME_MARKER_INTER, + FrameMarker::Final => FRAME_MARKER_FINAL, + FrameMarker::Start => FRAME_MARKER_START, + FrameMarker::Single => FRAME_MARKER_SINGLE, + }; + initial |= frame_marker << FRAME_MARKER_SHIFT; buf.put_u8(initial); buf.put_u8(metrics.ext_words as u8); @@ -134,14 +139,14 @@ impl Header { #[cfg(test)] mod tests { - use crate::dtp::{Dtp, Header, E2ee, Timestamp}; + use crate::dtp::{Dtp, E2ee, FrameMarker, Header, Timestamp}; use bytes::Buf; /// Constructed packet to use in tests. fn packet() -> Dtp { Dtp { header: Header { - is_final: true, + frame_marker: FrameMarker::Final, track_handle: 0x8811u32.try_into().unwrap(), sequence: 0x4422, frame_number: 0x4411, @@ -167,7 +172,7 @@ mod tests { assert_eq!(buf.len(), 1060); // Base header - assert_eq!(buf.get_u8(), 0x10); // Version 0, final flag set + assert_eq!(buf.get_u8(), 0x8); // Version 0, final flag set assert_eq!(buf.get_u8(), 6); // Extension words assert_eq!(buf.get_u16(), 0x8811); // Track handle assert_eq!(buf.get_u16(), 0x4422); // Sequence diff --git a/livekit-datatrack/src/local/packetizer.rs b/livekit-datatrack/src/local/packetizer.rs index 25f1a0847..36eec0b76 100644 --- a/livekit-datatrack/src/local/packetizer.rs +++ b/livekit-datatrack/src/local/packetizer.rs @@ -13,10 +13,7 @@ // limitations under the License. use crate::{ - dtp::{ - Clock, Timestamp, - Dtp, E2ee, Header, Handle, - }, + dtp::{Clock, Dtp, E2ee, FrameMarker, Handle, Header, Timestamp}, utils::{BytesChunkExt, Counter}, }; use bytes::Bytes; @@ -61,7 +58,7 @@ impl Packetizer { pub fn packetize(&mut self, frame: PacketizerFrame) -> Result, PacketizerError> { // TODO: consider using default let header = Header { - is_final: false, + frame_marker: FrameMarker::Inter, track_handle: self.handle, sequence: 0, frame_number: self.frame_number.get_then_increment(), @@ -81,7 +78,7 @@ impl Packetizer { .enumerate() .map(|(index, payload)| Dtp { header: Header { - is_final: index == packet_count - 1, + frame_marker: Self::frame_marker(index, packet_count), sequence: self.sequence.get_then_increment(), ..header }, @@ -90,6 +87,17 @@ impl Packetizer { .collect(); Ok(packets) } + + fn frame_marker(index: usize, packet_count: usize) -> FrameMarker { + if packet_count == 1 { + return FrameMarker::Single; + } + match index { + 0 => FrameMarker::Start, + _ if index == packet_count - 1 => FrameMarker::Final, + _ => FrameMarker::Inter + } + } } #[cfg(test)] @@ -127,6 +135,6 @@ mod tests { assert_eq!(packet.header.e2ee, with_exts.then_some(e2ee)); assert_eq!(packet.header.user_timestamp, with_exts.then_some(user_timestamp)); } - assert!(packets.last().unwrap().is_final()); + assert_eq!(packets.last().unwrap().header.frame_marker, FrameMarker::Final); } } From d08aa0885c0d37d55d44408c92fbf9f91472a02a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:10:16 +1100 Subject: [PATCH 107/232] Remove unused function --- livekit-datatrack/src/dtp/mod.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index e813f0494..e73862a75 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -66,13 +66,6 @@ pub struct E2ee { pub iv: [u8; 12], } -impl Dtp { - /// Whether the packet's payload is encrypted. - pub fn is_encrypted(&self) -> bool { - self.header.e2ee.is_some() - } -} - impl fmt::Debug for Dtp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Dtp") From f3c4545cb186898bc38cf0a17f671c034da4ccb1 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:11:25 +1100 Subject: [PATCH 108/232] Simplify docs --- livekit-datatrack/src/dtp/mod.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index e73862a75..a46433ad1 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -42,21 +42,16 @@ pub struct Header { pub e2ee: Option, } +/// Marker indicating a packet's position in relation to a frame. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FrameMarker { /// Packet is within a frame. - /// - /// This corresponds to neither the start nor final flag being set. - /// Inter, /// Packet is the last in a frame. Final, /// Packet is the first in a frame. Start, - /// Packet is the only one in the frame. - /// - /// This corresponds to both the start and final flag being set. - /// + /// Packet is the only one in a frame. Single } From 0cb44f384348c4aa463f897ef29b49a05be931da Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:34:46 +1100 Subject: [PATCH 109/232] Extensions struct --- livekit-datatrack/src/dtp/deserialize.rs | 44 ++++++++++----------- livekit-datatrack/src/dtp/mod.rs | 17 ++++++-- livekit-datatrack/src/dtp/serialize.rs | 48 ++++++++++++++++------- livekit-datatrack/src/local/packetizer.rs | 29 ++++++++------ livekit-datatrack/src/local/pipeline.rs | 15 ++++--- livekit-datatrack/src/remote/pipeline.rs | 6 +-- 6 files changed, 97 insertions(+), 62 deletions(-) diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs index 610fee0e8..c857762f8 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{consts::*, Dtp, E2ee, FrameMarker, Handle, HandleError, Header, Timestamp}; +use super::{ + consts::*, Dtp, E2eeExt, Extensions, FrameMarker, Handle, HandleError, Header, Timestamp, + UserTimestampExt, +}; use bytes::{Buf, Bytes}; use thiserror::Error; @@ -37,11 +40,11 @@ pub enum DeserializeError { InvalidExtId(u8), } -#[derive(Debug, Default)] -struct Extensions { - user_timestamp: Option, - e2ee: Option, -} +// #[derive(Debug, Default)] +// struct Extensions { +// user_timestamp: Option, +// e2ee: Option, +// } impl Dtp { pub fn deserialize(mut raw: Bytes) -> Result { @@ -81,23 +84,16 @@ impl Header { Err(DeserializeError::HeaderOverrun)? } let ext_block = raw.copy_to_bytes(ext_len); - let extensions = Extensions::parse(ext_block)?; - - let header = Header { - frame_marker, - track_handle, - sequence, - frame_number, - timestamp, - user_timestamp: extensions.user_timestamp, - e2ee: extensions.e2ee, - }; + let extensions = Extensions::deserialize(ext_block)?; + + let header = + Header { frame_marker, track_handle, sequence, frame_number, timestamp, extensions }; Ok(header) } } impl Extensions { - fn parse(mut raw: impl Buf) -> Result { + fn deserialize(mut raw: impl Buf) -> Result { let mut extensions = Self::default(); while raw.remaining() > 0 { let initial = raw.get_u8(); @@ -114,13 +110,13 @@ impl Extensions { let key_index = raw.get_u8(); let mut iv = [0u8; 12]; raw.copy_to_slice(&mut iv); - extensions.e2ee = E2ee { key_index, iv }.into(); + extensions.e2ee = E2eeExt { key_index, iv }.into(); } EXT_ID_USER_TIMESTAMP => { if raw.remaining() < EXT_LEN_USER_TIMESTAMP { Err(DeserializeError::MalformedExt(ext_id))? } - extensions.user_timestamp = raw.get_u64().into() + extensions.user_timestamp = UserTimestampExt(raw.get_u64()).into() } EXT_ID_INVALID => Err(DeserializeError::InvalidExtId(EXT_ID_INVALID))?, _ => { @@ -193,8 +189,8 @@ mod tests { assert_eq!(dtp.header.sequence, 0x4422); assert_eq!(dtp.header.frame_number, 0x4411); assert_eq!(dtp.header.timestamp, Timestamp::from_ticks(0x44221188)); - assert_eq!(dtp.header.user_timestamp, None); - assert_eq!(dtp.header.e2ee, None); + assert_eq!(dtp.header.extensions.user_timestamp, None); + assert_eq!(dtp.header.extensions.e2ee, None); } #[test] @@ -215,7 +211,7 @@ mod tests { raw.put_bytes(0x00, 2); // Padding let dtp = Dtp::deserialize(raw.freeze()).unwrap(); - let e2ee = dtp.header.e2ee.unwrap(); + let e2ee = dtp.header.extensions.e2ee.unwrap(); assert_eq!(e2ee.key_index, 0xFA); assert_eq!(e2ee.iv, [0x3C; 12]); } @@ -229,7 +225,7 @@ mod tests { raw.put_bytes(0x00, 3); // Padding // TODO: decreasing to 2 is header overrun (should be padding error) let dtp = Dtp::deserialize(raw.freeze()).unwrap(); - assert_eq!(dtp.header.user_timestamp, Some(0x4411221111118811)); + assert_eq!(dtp.header.extensions.user_timestamp, UserTimestampExt(0x4411221111118811).into()); } #[test] diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index a46433ad1..59942f8cb 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -38,10 +38,10 @@ pub struct Header { pub sequence: u16, pub frame_number: u16, pub timestamp: Timestamp<90_000>, - pub user_timestamp: Option, - pub e2ee: Option, + pub extensions: Extensions } + /// Marker indicating a packet's position in relation to a frame. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FrameMarker { @@ -55,8 +55,17 @@ pub enum FrameMarker { Single } +#[derive(Debug, Clone, Default)] +pub struct Extensions { + pub user_timestamp: Option, + pub e2ee: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UserTimestampExt(pub u64); + #[derive(Clone, Copy, PartialEq, Eq)] -pub struct E2ee { +pub struct E2eeExt { pub key_index: u8, pub iv: [u8; 12], } @@ -70,7 +79,7 @@ impl fmt::Debug for Dtp { } } -impl fmt::Debug for E2ee { +impl fmt::Debug for E2eeExt { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // For security, do not include fields in debug. f.debug_struct("E2ee").finish() diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/dtp/serialize.rs index aa1adfa5a..914cd35ba 100644 --- a/livekit-datatrack/src/dtp/serialize.rs +++ b/livekit-datatrack/src/dtp/serialize.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{consts::*, Dtp, FrameMarker, Header}; +use super::{consts::*, Dtp, E2eeExt, Extensions, FrameMarker, Header, UserTimestampExt}; use bytes::{BufMut, Bytes, BytesMut}; use thiserror::Error; @@ -83,18 +83,36 @@ impl Header { buf.put_u16(self.frame_number); buf.put_u32(self.timestamp.as_ticks()); - if let Some(e2ee) = &self.e2ee { - buf.put_u8(EXT_MARKER_E2EE); - buf.put_u8(e2ee.key_index); - buf.put_slice(&e2ee.iv); + self.extensions.serialize_into(buf); + buf.put_bytes(0, metrics.padding_len); + + Ok(metrics.len) + } +} + +impl Extensions { + fn serialize_into(self, buf: &mut impl BufMut) { + if let Some(e2ee) = self.e2ee { + e2ee.serialize_into(buf); } if let Some(user_timestamp) = self.user_timestamp { - buf.put_u8(EXT_MARKER_USER_TIMESTAMP); - buf.put_u64(user_timestamp); + user_timestamp.serialize_into(buf); } - buf.put_bytes(0, metrics.padding_len); + } +} - Ok(metrics.len) +impl E2eeExt { + fn serialize_into(self, buf: &mut impl BufMut) { + buf.put_u8(EXT_MARKER_E2EE); + buf.put_u8(self.key_index); + buf.put_slice(&self.iv); + } +} + +impl UserTimestampExt { + fn serialize_into(self, buf: &mut impl BufMut) { + buf.put_u8(EXT_MARKER_USER_TIMESTAMP); + buf.put_u64(self.0); } } @@ -117,10 +135,10 @@ impl Header { /// Length of all extensions not including padding. fn ext_len(&self) -> usize { let mut len = 0; - if self.e2ee.is_some() { + if self.extensions.e2ee.is_some() { len += EXT_MARKER_LEN + EXT_LEN_E2EE; } - if self.user_timestamp.is_some() { + if self.extensions.user_timestamp.is_some() { len += EXT_MARKER_LEN + EXT_LEN_USER_TIMESTAMP; } len @@ -139,7 +157,7 @@ impl Header { #[cfg(test)] mod tests { - use crate::dtp::{Dtp, E2ee, FrameMarker, Header, Timestamp}; + use crate::dtp::{Dtp, E2eeExt, Extensions, FrameMarker, Header, Timestamp, UserTimestampExt}; use bytes::Buf; /// Constructed packet to use in tests. @@ -151,8 +169,10 @@ mod tests { sequence: 0x4422, frame_number: 0x4411, timestamp: Timestamp::from_ticks(0x44221188), - user_timestamp: 0x4411221111118811.into(), - e2ee: E2ee { key_index: 0xFA, iv: [0x3C; 12] }.into(), + extensions: Extensions { + user_timestamp: UserTimestampExt(0x4411221111118811).into(), + e2ee: E2eeExt { key_index: 0xFA, iv: [0x3C; 12] }.into(), + } }, payload: vec![0xFA; 1024].into(), } diff --git a/livekit-datatrack/src/local/packetizer.rs b/livekit-datatrack/src/local/packetizer.rs index 36eec0b76..728d12326 100644 --- a/livekit-datatrack/src/local/packetizer.rs +++ b/livekit-datatrack/src/local/packetizer.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::{ - dtp::{Clock, Dtp, E2ee, FrameMarker, Handle, Header, Timestamp}, + dtp::{Clock, Dtp, Extensions, FrameMarker, Handle, Header, Timestamp}, utils::{BytesChunkExt, Counter}, }; use bytes::Bytes; @@ -32,8 +32,7 @@ pub struct Packetizer { /// Frame packetized by [`Packetizer`]. pub struct PacketizerFrame { pub payload: Bytes, - pub e2ee: Option, - pub user_timestamp: Option, + pub extensions: Extensions, } #[derive(Error, Debug)] @@ -63,8 +62,7 @@ impl Packetizer { sequence: 0, frame_number: self.frame_number.get_then_increment(), timestamp: self.clock.now(), - user_timestamp: frame.user_timestamp, - e2ee: frame.e2ee, + extensions: frame.extensions, }; let max_payload_size = self.mtu_size.saturating_sub(header.serialized_len()); if max_payload_size == 0 { @@ -80,6 +78,7 @@ impl Packetizer { header: Header { frame_marker: Self::frame_marker(index, packet_count), sequence: self.sequence.get_then_increment(), + extensions: header.extensions.clone(), ..header }, payload, @@ -95,7 +94,7 @@ impl Packetizer { match index { 0 => FrameMarker::Start, _ if index == packet_count - 1 => FrameMarker::Final, - _ => FrameMarker::Inter + _ => FrameMarker::Inter, } } } @@ -103,6 +102,7 @@ impl Packetizer { #[cfg(test)] mod tests { use super::*; + use crate::dtp::{E2eeExt, UserTimestampExt}; use rstest::rstest; #[rstest] @@ -112,15 +112,17 @@ mod tests { #[values(true, false)] with_exts: bool, ) { let handle = 1u32.try_into().unwrap(); - let e2ee = E2ee { key_index: 255, iv: [0xCD; 12] }; - let user_timestamp = u64::MAX; + let e2ee = E2eeExt { key_index: 255, iv: [0xCD; 12] }; + let user_timestamp = UserTimestampExt(u64::MAX); let mut packetizer = Packetizer::new(handle, mtu_size); let frame = PacketizerFrame { payload: Bytes::from(vec![0xAB; payload_size]), - e2ee: with_exts.then_some(e2ee), - user_timestamp: with_exts.then_some(user_timestamp), + extensions: Extensions { + e2ee: with_exts.then_some(e2ee), + user_timestamp: with_exts.then_some(user_timestamp), + }, }; let packets = packetizer.packetize(frame).expect("Failed to packetize"); @@ -132,8 +134,11 @@ mod tests { for (index, packet) in packets.iter().enumerate() { assert_eq!(packet.header.frame_number, 0); assert_eq!(packet.header.sequence, index as u16); - assert_eq!(packet.header.e2ee, with_exts.then_some(e2ee)); - assert_eq!(packet.header.user_timestamp, with_exts.then_some(user_timestamp)); + assert_eq!(packet.header.extensions.e2ee, with_exts.then_some(e2ee)); + assert_eq!( + packet.header.extensions.user_timestamp, + with_exts.then_some(user_timestamp) + ); } assert_eq!(packets.last().unwrap().header.frame_marker, FrameMarker::Final); } diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 37ed47d5b..1447d02f2 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -15,7 +15,7 @@ use super::packetizer::{Packetizer, PacketizerFrame}; use crate::{ api::{DataTrackFrame, DataTrackInfo}, - dtp, + dtp::{self, Extensions, UserTimestampExt}, e2ee::EncryptionProvider, local::manager::{LocalTrackState, OutputEvent, UnpublishInitiator, UnpublishRequestEvent}, }; @@ -54,7 +54,7 @@ impl LocalTrackTask { } fn publish_frame(&mut self, mut frame: DataTrackFrame) { - let mut e2ee: Option = None; + let mut e2ee: Option = None; if let Some(encryption) = &self.encryption { debug_assert!(self.info.uses_e2ee); let encrypted_payload = match encryption.encrypt(frame.payload) { @@ -64,15 +64,20 @@ impl LocalTrackTask { return; } }; - e2ee = Some(dtp::E2ee { + e2ee = Some(dtp::E2eeExt { key_index: encrypted_payload.key_index, iv: encrypted_payload.iv, }); frame.payload = encrypted_payload.payload; } - let frame = - PacketizerFrame { payload: frame.payload, e2ee, user_timestamp: frame.user_timestamp }; + let frame = PacketizerFrame { + payload: frame.payload, + extensions: Extensions { + e2ee, + user_timestamp: frame.user_timestamp.map(|v| UserTimestampExt(v)), + }, + }; let packets = match self.packetizer.packetize(frame) { Ok(packets) => packets, diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 4123859cb..d1a1d9c71 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -53,14 +53,14 @@ impl RemoteTrackTask { if let Some(decryption) = &self.decryption { debug_assert!(self.info.uses_e2ee); - let Some(e2ee_meta) = dtp.header.e2ee else { + let Some(e2ee) = dtp.header.extensions.e2ee else { log::error!("Missing E2EE meta"); return; }; let encrypted_payload = EncryptedPayload { payload: dtp.payload, - iv: e2ee_meta.iv, - key_index: e2ee_meta.key_index, + iv: e2ee.iv, + key_index: e2ee.key_index, }; let decrypted_payload = match decryption.decrypt(encrypted_payload) { Ok(decrypted_payload) => decrypted_payload, From 59d80eed1c3c39f05d315404ce52dbf0c74f3ea7 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 9 Jan 2026 12:37:53 +1100 Subject: [PATCH 110/232] Rename --- livekit-datatrack/src/dtp/deserialize.rs | 6 +++--- livekit-datatrack/src/dtp/mod.rs | 2 +- livekit-datatrack/src/dtp/serialize.rs | 6 +++--- livekit-datatrack/src/local/packetizer.rs | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs index c857762f8..7f50059c8 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -66,7 +66,7 @@ impl Header { if version > SUPPORTED_VERSION { Err(DeserializeError::UnsupportedVersion(version))? } - let frame_marker = match initial >> FRAME_MARKER_SHIFT & FRAME_MARKER_MASK { + let marker = match initial >> FRAME_MARKER_SHIFT & FRAME_MARKER_MASK { FRAME_MARKER_START => FrameMarker::Start, FRAME_MARKER_FINAL => FrameMarker::Final, FRAME_MARKER_SINGLE => FrameMarker::Single, @@ -87,7 +87,7 @@ impl Header { let extensions = Extensions::deserialize(ext_block)?; let header = - Header { frame_marker, track_handle, sequence, frame_number, timestamp, extensions }; + Header { marker, track_handle, sequence, frame_number, timestamp, extensions }; Ok(header) } } @@ -184,7 +184,7 @@ mod tests { raw.put_slice(&[0x44, 0x22, 0x11, 0x88]); // Timestamp let dtp = Dtp::deserialize(raw.freeze()).unwrap(); - assert_eq!(dtp.header.frame_marker, FrameMarker::Final); + assert_eq!(dtp.header.marker, FrameMarker::Final); assert_eq!(dtp.header.track_handle, 0x8811u32.try_into().unwrap()); assert_eq!(dtp.header.sequence, 0x4422); assert_eq!(dtp.header.frame_number, 0x4411); diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index 59942f8cb..4b94cba52 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -33,7 +33,7 @@ pub struct Dtp { #[derive(Debug, Clone)] pub struct Header { - pub frame_marker: FrameMarker, + pub marker: FrameMarker, pub track_handle: Handle, pub sequence: u16, pub frame_number: u16, diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/dtp/serialize.rs index 914cd35ba..8c157975e 100644 --- a/livekit-datatrack/src/dtp/serialize.rs +++ b/livekit-datatrack/src/dtp/serialize.rs @@ -68,13 +68,13 @@ impl Header { let mut initial = SUPPORTED_VERSION << VERSION_SHIFT; - let frame_marker = match self.frame_marker { + let marker = match self.marker { FrameMarker::Inter => FRAME_MARKER_INTER, FrameMarker::Final => FRAME_MARKER_FINAL, FrameMarker::Start => FRAME_MARKER_START, FrameMarker::Single => FRAME_MARKER_SINGLE, }; - initial |= frame_marker << FRAME_MARKER_SHIFT; + initial |= marker << FRAME_MARKER_SHIFT; buf.put_u8(initial); buf.put_u8(metrics.ext_words as u8); @@ -164,7 +164,7 @@ mod tests { fn packet() -> Dtp { Dtp { header: Header { - frame_marker: FrameMarker::Final, + marker: FrameMarker::Final, track_handle: 0x8811u32.try_into().unwrap(), sequence: 0x4422, frame_number: 0x4411, diff --git a/livekit-datatrack/src/local/packetizer.rs b/livekit-datatrack/src/local/packetizer.rs index 728d12326..1287fca19 100644 --- a/livekit-datatrack/src/local/packetizer.rs +++ b/livekit-datatrack/src/local/packetizer.rs @@ -57,7 +57,7 @@ impl Packetizer { pub fn packetize(&mut self, frame: PacketizerFrame) -> Result, PacketizerError> { // TODO: consider using default let header = Header { - frame_marker: FrameMarker::Inter, + marker: FrameMarker::Inter, track_handle: self.handle, sequence: 0, frame_number: self.frame_number.get_then_increment(), @@ -76,7 +76,7 @@ impl Packetizer { .enumerate() .map(|(index, payload)| Dtp { header: Header { - frame_marker: Self::frame_marker(index, packet_count), + marker: Self::frame_marker(index, packet_count), sequence: self.sequence.get_then_increment(), extensions: header.extensions.clone(), ..header @@ -140,6 +140,6 @@ mod tests { with_exts.then_some(user_timestamp) ); } - assert_eq!(packets.last().unwrap().header.frame_marker, FrameMarker::Final); + assert_eq!(packets.last().unwrap().header.marker, FrameMarker::Final); } } From ea193a2ef37f4367e59dcc3146d1ebf92ad3c49c Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 9 Jan 2026 12:39:55 +1100 Subject: [PATCH 111/232] Remove comment --- livekit-datatrack/src/dtp/deserialize.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs index 7f50059c8..c3b0643b4 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -40,12 +40,6 @@ pub enum DeserializeError { InvalidExtId(u8), } -// #[derive(Debug, Default)] -// struct Extensions { -// user_timestamp: Option, -// e2ee: Option, -// } - impl Dtp { pub fn deserialize(mut raw: Bytes) -> Result { let header = Header::deserialize(&mut raw)?; From cffb27b2e8d89f66b4521af9997e3854ac394ca7 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:37:06 +1100 Subject: [PATCH 112/232] Separate extension module --- livekit-datatrack/src/dtp/extension.rs | 37 ++++++++++++++++++++++++++ livekit-datatrack/src/dtp/mod.rs | 24 ++--------------- 2 files changed, 39 insertions(+), 22 deletions(-) create mode 100644 livekit-datatrack/src/dtp/extension.rs diff --git a/livekit-datatrack/src/dtp/extension.rs b/livekit-datatrack/src/dtp/extension.rs new file mode 100644 index 000000000..bdfe070c2 --- /dev/null +++ b/livekit-datatrack/src/dtp/extension.rs @@ -0,0 +1,37 @@ +// Copyright 2025 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 core::fmt; + +#[derive(Debug, Clone, Default)] +pub struct Extensions { + pub user_timestamp: Option, + pub e2ee: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UserTimestampExt(pub u64); + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct E2eeExt { + pub key_index: u8, + pub iv: [u8; 12], +} + +impl fmt::Debug for E2eeExt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // For security, do not include fields in debug. + f.debug_struct("E2ee").finish() + } +} \ No newline at end of file diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index 4b94cba52..4ce4e899f 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -15,11 +15,13 @@ use bytes::Bytes; use core::fmt; +mod extension; mod deserialize; mod handle; mod serialize; mod time; +pub use extension::*; pub use deserialize::*; pub use handle::*; pub use serialize::*; @@ -55,21 +57,6 @@ pub enum FrameMarker { Single } -#[derive(Debug, Clone, Default)] -pub struct Extensions { - pub user_timestamp: Option, - pub e2ee: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct UserTimestampExt(pub u64); - -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct E2eeExt { - pub key_index: u8, - pub iv: [u8; 12], -} - impl fmt::Debug for Dtp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Dtp") @@ -79,13 +66,6 @@ impl fmt::Debug for Dtp { } } -impl fmt::Debug for E2eeExt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // For security, do not include fields in debug. - f.debug_struct("E2ee").finish() - } -} - /// Constants used for serialization and deserialization. pub(crate) mod consts { pub const SUPPORTED_VERSION: u8 = 0; From ffb84e214414c402f82d98585177b8d91d215a99 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 10 Jan 2026 14:31:22 +1100 Subject: [PATCH 113/232] Dtp revisions --- livekit-datatrack/src/dtp/deserialize.rs | 150 ++++++++++++--------- livekit-datatrack/src/dtp/extension.rs | 12 ++ livekit-datatrack/src/dtp/mod.rs | 23 +--- livekit-datatrack/src/dtp/serialize.rs | 158 +++++++++++++---------- 4 files changed, 191 insertions(+), 152 deletions(-) diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs index c3b0643b4..c404c96a1 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -13,8 +13,8 @@ // limitations under the License. use super::{ - consts::*, Dtp, E2eeExt, Extensions, FrameMarker, Handle, HandleError, Header, Timestamp, - UserTimestampExt, + consts::*, Dtp, E2eeExt, ExtensionTag, Extensions, FrameMarker, Handle, HandleError, Header, + Timestamp, UserTimestampExt, }; use bytes::{Buf, Bytes}; use thiserror::Error; @@ -27,17 +27,17 @@ pub enum DeserializeError { #[error("header exceeds total packet length")] HeaderOverrun, + #[error("extension word indicator is missing")] + MissingExtWords, + #[error("unsupported version {0}")] UnsupportedVersion(u8), #[error("invalid track handle: {0}")] InvalidHandle(#[from] HandleError), - #[error("extension with id {0} is malformed")] - MalformedExt(u8), - - #[error("{0} is not a valid extension id")] - InvalidExtId(u8), + #[error("extension with tag {0} is malformed")] + MalformedExt(ExtensionTag), } impl Dtp { @@ -66,22 +66,30 @@ impl Header { FRAME_MARKER_SINGLE => FrameMarker::Single, _ => FrameMarker::Inter, }; - let extension_words = raw.get_u8(); - let ext_len = 4 * extension_words as usize; + let ext_flag = (initial >> EXT_FLAG_SHIFT & EXT_FLAG_MASK) > 0; + raw.advance(1); // Reserved let track_handle: Handle = raw.get_u16().try_into()?; let sequence = raw.get_u16(); let frame_number = raw.get_u16(); let timestamp = Timestamp::from_ticks(raw.get_u32()); - if ext_len > raw.remaining() { - Err(DeserializeError::HeaderOverrun)? + let mut extensions = Extensions::default(); + if ext_flag { + if raw.remaining() < 2 { + Err(DeserializeError::MissingExtWords)?; + } + let ext_words = raw.get_u16(); + + let ext_len = 4 * ext_words as usize; + if ext_len > raw.remaining() { + Err(DeserializeError::HeaderOverrun)? + } + let ext_block = raw.copy_to_bytes(ext_len); + extensions = Extensions::deserialize(ext_block)?; } - let ext_block = raw.copy_to_bytes(ext_len); - let extensions = Extensions::deserialize(ext_block)?; - let header = - Header { marker, track_handle, sequence, frame_number, timestamp, extensions }; + let header = Header { marker, track_handle, sequence, frame_number, timestamp, extensions }; Ok(header) } } @@ -89,37 +97,35 @@ impl Header { impl Extensions { fn deserialize(mut raw: impl Buf) -> Result { let mut extensions = Self::default(); - while raw.remaining() > 0 { - let initial = raw.get_u8(); - if initial == 0 { + while raw.remaining() >= 4 { + let tag = raw.get_u16(); + let len = raw.get_u16() as usize; + if tag == EXT_TAG_PADDING { // Skip padding continue; } - let ext_id = initial >> 4; - match ext_id { - EXT_ID_E2EE => { - if raw.remaining() < EXT_LEN_E2EE { - Err(DeserializeError::MalformedExt(ext_id))? + match tag { + E2eeExt::TAG => { + if raw.remaining() < E2eeExt::LEN { + Err(DeserializeError::MalformedExt(tag))? } let key_index = raw.get_u8(); let mut iv = [0u8; 12]; raw.copy_to_slice(&mut iv); extensions.e2ee = E2eeExt { key_index, iv }.into(); } - EXT_ID_USER_TIMESTAMP => { - if raw.remaining() < EXT_LEN_USER_TIMESTAMP { - Err(DeserializeError::MalformedExt(ext_id))? + UserTimestampExt::TAG => { + if raw.remaining() < UserTimestampExt::LEN { + Err(DeserializeError::MalformedExt(tag))? } extensions.user_timestamp = UserTimestampExt(raw.get_u64()).into() } - EXT_ID_INVALID => Err(DeserializeError::InvalidExtId(EXT_ID_INVALID))?, _ => { // Skip over unknown extensions (forward compatible). - let ext_len = ((initial & (0xFF ^ 0xF0)) + 1) as usize; - if raw.remaining() < ext_len { - Err(DeserializeError::MalformedExt(ext_id))? + if raw.remaining() < len { + Err(DeserializeError::MalformedExt(tag))? } - raw.advance(ext_len); + raw.advance(len); continue; } } @@ -132,11 +138,12 @@ impl Extensions { mod tests { use super::*; use bytes::{BufMut, BytesMut}; + use rstest::*; /// Returns the simplest valid packet to use in test. fn valid_packet() -> BytesMut { let mut raw = BytesMut::zeroed(12); // Base header - raw[3] = 1; // Non-zero track ID + raw[3] = 1; // Non-zero track handle raw } @@ -149,10 +156,21 @@ mod tests { assert!(matches!(dtp, Err(DeserializeError::TooShort))); } + #[test] + fn test_missing_ext_words() { + let mut raw = valid_packet(); + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + // Should have ext word indicator here + + let dtp = Dtp::deserialize(raw.freeze()); + assert!(matches!(dtp, Err(DeserializeError::MissingExtWords))); + } + #[test] fn test_header_overrun() { let mut raw = valid_packet(); - raw[1] = 1; // 1 extension word, would overrun buffer + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(1); // One extension word let dtp = Dtp::deserialize(raw.freeze()); assert!(matches!(dtp, Err(DeserializeError::HeaderOverrun))); @@ -170,8 +188,8 @@ mod tests { #[test] fn test_base_header() { let mut raw = BytesMut::new(); - raw.put_u8(0x8); // Version 0, start flag set - raw.put_u8(0x0); // No extension words + raw.put_u8(0x8); // Version 0, final flag set, no extensions + raw.put_u8(0x0); // Reserved raw.put_slice(&[0x88, 0x11]); // Track ID raw.put_slice(&[0x44, 0x22]); // Sequence raw.put_slice(&[0x44, 0x11]); // Frame number @@ -187,22 +205,29 @@ mod tests { assert_eq!(dtp.header.extensions.e2ee, None); } - #[test] - fn test_ext_skips_padding() { + #[rstest] + fn test_ext_skips_padding(#[values(0, 1, 24)] ext_words: usize) { let mut raw = valid_packet(); - raw[1] = 4; // 1 extension word - raw.put_bytes(0x00, 32 * 4); // Padding - Dtp::deserialize(raw.freeze()).unwrap(); + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + + raw.put_u16(ext_words as u16); // 4 extension word + raw.put_bytes(0, ext_words * 4); // Padding + + let dtp = Dtp::deserialize(raw.freeze()).unwrap(); + assert_eq!(dtp.payload.len(), 0); } #[test] fn test_ext_e2ee() { let mut raw = valid_packet(); - raw[1] = 4; // 4 extension words - raw.put_u8(0x1C); // ID 1, length 12 + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(5); // Extension words + + raw.put_u16(1); // ID 1 + raw.put_u16(12); // Length 12 raw.put_u8(0xFA); // Key index raw.put_bytes(0x3C, 12); // IV - raw.put_bytes(0x00, 2); // Padding + raw.put_bytes(0, 3); // Padding let dtp = Dtp::deserialize(raw.freeze()).unwrap(); let e2ee = dtp.header.extensions.e2ee.unwrap(); @@ -213,40 +238,37 @@ mod tests { #[test] fn test_ext_user_timestamp() { let mut raw = valid_packet(); - raw[1] = 3; // 3 extension words - raw.put_u8(0x27); // ID 2, length 7 + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(3); // Extension words + + raw.put_u16(2); + raw.put_u16(7); raw.put_slice(&[0x44, 0x11, 0x22, 0x11, 0x11, 0x11, 0x88, 0x11]); // User timestamp - raw.put_bytes(0x00, 3); // Padding - // TODO: decreasing to 2 is header overrun (should be padding error) + let dtp = Dtp::deserialize(raw.freeze()).unwrap(); - assert_eq!(dtp.header.extensions.user_timestamp, UserTimestampExt(0x4411221111118811).into()); + assert_eq!( + dtp.header.extensions.user_timestamp, + UserTimestampExt(0x4411221111118811).into() + ); } #[test] fn test_ext_unknown() { let mut raw = valid_packet(); - raw[1] = 1; // 1 extension word - raw.put_u8(0x80); // ID 8 (unknown) - raw.put_bytes(0x00, 3); // Padding - Dtp::deserialize(raw.freeze()).expect("Should skip unknown extension"); - } - - #[test] - fn test_ext_id_invalid() { - let mut raw = valid_packet(); - raw[1] = 1; // 1 extension word - raw.put_u8(0xF0); // ID 15, invalid - raw.put_bytes(0x00, 3); // Padding + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(2); // Extension words - let dtp = Dtp::deserialize(raw.freeze()); - assert!(matches!(dtp, Err(DeserializeError::InvalidExtId(15)))); + raw.put_u16(8); // ID 8 (unknown) + raw.put_bytes(0, 6); + Dtp::deserialize(raw.freeze()).expect("Should skip unknown extension"); } #[test] fn test_ext_required_word_alignment() { let mut raw = valid_packet(); - raw[1] = 1; // 1 extension word - raw.put_bytes(0x00, 3); // Padding, missing one byte + raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag + raw.put_u16(1); // Extension words + raw.put_bytes(0, 3); // Padding, missing one byte assert!(Dtp::deserialize(raw.freeze()).is_err()); } diff --git a/livekit-datatrack/src/dtp/extension.rs b/livekit-datatrack/src/dtp/extension.rs index bdfe070c2..b8ffc70ff 100644 --- a/livekit-datatrack/src/dtp/extension.rs +++ b/livekit-datatrack/src/dtp/extension.rs @@ -34,4 +34,16 @@ impl fmt::Debug for E2eeExt { // For security, do not include fields in debug. f.debug_struct("E2ee").finish() } +} + +pub(super) type ExtensionTag = u16; + +impl UserTimestampExt { + pub(super) const TAG: ExtensionTag = 2; + pub(super) const LEN: usize = 8; +} + +impl E2eeExt { + pub(super) const TAG: ExtensionTag = 1; + pub(super) const LEN: usize = 13; } \ No newline at end of file diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index 4ce4e899f..5ba302349 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -83,22 +83,9 @@ pub(crate) mod consts { pub const FRAME_MARKER_INTER: u8 = 0x0; pub const FRAME_MARKER_SINGLE: u8 = 0x3; - // Extension IDs - pub const EXT_ID_E2EE: u8 = 0x1; - pub const EXT_ID_USER_TIMESTAMP: u8 = 0x2; - pub const EXT_ID_INVALID: u8 = 0xF; - - // Extension lengths - pub const EXT_LEN_E2EE: usize = 13; - pub const EXT_LEN_USER_TIMESTAMP: usize = 8; - - // Extension markers - pub const EXT_MARKER_LEN: usize = 1; - pub const EXT_MARKER_E2EE: u8 = ext_marker(EXT_ID_E2EE, EXT_LEN_E2EE as u8); - pub const EXT_MARKER_USER_TIMESTAMP: u8 = - ext_marker(EXT_ID_USER_TIMESTAMP, EXT_LEN_USER_TIMESTAMP as u8); - - const fn ext_marker(id: u8, len: u8) -> u8 { - (id << 4) | (len - 1) - } + pub const EXT_WORDS_INDICATOR_SIZE: usize = 2; + pub const EXT_FLAG_SHIFT: u8 = 0x2; + pub const EXT_FLAG_MASK: u8 = 0x1; + pub const EXT_MARKER_LEN: usize = 4; + pub const EXT_TAG_PADDING: u16 = 0; } diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/dtp/serialize.rs index 8c157975e..39f211699 100644 --- a/livekit-datatrack/src/dtp/serialize.rs +++ b/livekit-datatrack/src/dtp/serialize.rs @@ -26,14 +26,9 @@ pub enum SerializeError { } impl Dtp { - /// Serialize the packet into a new buffer. - pub fn serialize(self) -> Bytes { - let len = self.serialized_len(); - let mut buf = BytesMut::with_capacity(len); - - let written = self.serialize_into(&mut buf).unwrap(); - assert_eq!(written, len); - buf.freeze() + /// Length of the serialized packet in bytes. + pub fn serialized_len(&self) -> usize { + self.header.serialized_len() + self.payload.len() } /// Serialize the packet into the given buffer. @@ -44,30 +39,59 @@ impl Dtp { pub fn serialize_into(self, buf: &mut impl BufMut) -> Result { let payload_len = self.payload.len(); let header_len = self.header.serialize_into(buf)?; - if buf.remaining_mut() < self.payload.len() { + if buf.remaining_mut() < payload_len { Err(SerializeError::TooSmallForPayload)? } buf.put(self.payload); Ok(header_len + payload_len) } + + /// Serialize the packet into a new buffer. + pub fn serialize(self) -> Bytes { + let len = self.serialized_len(); + let mut buf = BytesMut::with_capacity(len); + + let written = self.serialize_into(&mut buf).unwrap(); + assert_eq!(written, len); + buf.freeze() + } } -impl Dtp { - /// Length of the serialized packet in bytes. - pub fn serialized_len(&self) -> usize { - self.header.serialized_len() + self.payload.len() +struct HeaderMetrics { + ext_len: usize, + ext_words: usize, + padding_len: usize, +} + +impl HeaderMetrics { + fn serialized_len(&self) -> usize { + BASE_HEADER_LEN + EXT_WORDS_INDICATOR_SIZE + self.ext_len + self.padding_len } } impl Header { + /// Lengths of individual elements in the serialized header. + fn metrics(&self) -> HeaderMetrics { + let ext_len = self.extensions.serialized_len(); + let ext_words = ext_len.div_ceil(4); + let padding_len = (ext_words * 4) - ext_len; + HeaderMetrics { ext_len, ext_words, padding_len } + } + + /// Length of the serialized header in bytes. + pub fn serialized_len(&self) -> usize { + self.metrics().serialized_len() + } + fn serialize_into(self, buf: &mut impl BufMut) -> Result { let metrics = self.metrics(); - if buf.remaining_mut() < metrics.len { + let serialized_len = metrics.serialized_len(); + + if buf.remaining_mut() < serialized_len { Err(SerializeError::TooSmallForHeader)? } let mut initial = SUPPORTED_VERSION << VERSION_SHIFT; - let marker = match self.marker { FrameMarker::Inter => FRAME_MARKER_INTER, FrameMarker::Final => FRAME_MARKER_FINAL, @@ -75,22 +99,42 @@ impl Header { FrameMarker::Single => FRAME_MARKER_SINGLE, }; initial |= marker << FRAME_MARKER_SHIFT; + + if metrics.ext_len > 0 { + initial |= 1 << EXT_FLAG_SHIFT; + } buf.put_u8(initial); - buf.put_u8(metrics.ext_words as u8); + buf.put_u8(0); // Reserved buf.put_u16(self.track_handle.into()); buf.put_u16(self.sequence); buf.put_u16(self.frame_number); buf.put_u32(self.timestamp.as_ticks()); - self.extensions.serialize_into(buf); - buf.put_bytes(0, metrics.padding_len); + if metrics.ext_len > 0 { + buf.put_u16(metrics.ext_words as u16); + self.extensions.serialize_into(buf); + buf.put_bytes(0, metrics.padding_len); + } - Ok(metrics.len) + // TODO: length assertion + Ok(serialized_len) } } impl Extensions { + /// Length of extensions excluding padding. + fn serialized_len(&self) -> usize { + let mut len = 0; + if self.e2ee.is_some() { + len += EXT_MARKER_LEN + E2eeExt::LEN; + } + if self.user_timestamp.is_some() { + len += EXT_MARKER_LEN + UserTimestampExt::LEN; + } + len + } + fn serialize_into(self, buf: &mut impl BufMut) { if let Some(e2ee) = self.e2ee { e2ee.serialize_into(buf); @@ -103,7 +147,8 @@ impl Extensions { impl E2eeExt { fn serialize_into(self, buf: &mut impl BufMut) { - buf.put_u8(EXT_MARKER_E2EE); + buf.put_u16(Self::TAG); + buf.put_u16(Self::LEN as u16 - 1); buf.put_u8(self.key_index); buf.put_slice(&self.iv); } @@ -111,50 +156,12 @@ impl E2eeExt { impl UserTimestampExt { fn serialize_into(self, buf: &mut impl BufMut) { - buf.put_u8(EXT_MARKER_USER_TIMESTAMP); + buf.put_u16(Self::TAG); + buf.put_u16(Self::LEN as u16 - 1); buf.put_u64(self.0); } } -#[derive(Debug)] -struct HeaderMetrics { - /// Number of 32-bit extension words. - ext_words: usize, - /// Number of padding bytes needed to align extension block. - padding_len: usize, - /// Total size of the serialized header in bytes - len: usize, -} - -impl Header { - /// Length of the serialized header in bytes. - pub fn serialized_len(&self) -> usize { - self.metrics().len - } - - /// Length of all extensions not including padding. - fn ext_len(&self) -> usize { - let mut len = 0; - if self.extensions.e2ee.is_some() { - len += EXT_MARKER_LEN + EXT_LEN_E2EE; - } - if self.extensions.user_timestamp.is_some() { - len += EXT_MARKER_LEN + EXT_LEN_USER_TIMESTAMP; - } - len - } - - /// Header metrics required for buffer sizing and serialization. - fn metrics(&self) -> HeaderMetrics { - let ext_len = self.ext_len(); - let ext_words = ext_len.div_ceil(4); - assert!(ext_words <= u8::MAX.into()); - let padding_len = (ext_words * 4) - ext_len; - let len = BASE_HEADER_LEN + ext_len + padding_len; - HeaderMetrics { ext_words, padding_len, len } - } -} - #[cfg(test)] mod tests { use crate::dtp::{Dtp, E2eeExt, Extensions, FrameMarker, Header, Timestamp, UserTimestampExt}; @@ -172,7 +179,7 @@ mod tests { extensions: Extensions { user_timestamp: UserTimestampExt(0x4411221111118811).into(), e2ee: E2eeExt { key_index: 0xFA, iv: [0x3C; 12] }.into(), - } + }, }, payload: vec![0xFA; 1024].into(), } @@ -181,34 +188,45 @@ mod tests { #[test] fn test_header_metrics() { let metrics = packet().header.metrics(); - assert_eq!(metrics.ext_words, 6); - assert_eq!(metrics.padding_len, 1); - assert_eq!(metrics.len, 36); + assert_eq!(metrics.ext_len, 29); + assert_eq!(metrics.ext_words, 8); + assert_eq!(metrics.padding_len, 3); + } + + #[test] + fn test_serialized_length() { + let dtp = packet(); + assert_eq!(dtp.serialized_len(), 1070); + assert_eq!(dtp.header.serialized_len(), 46); + assert_eq!(dtp.header.extensions.serialized_len(), 29); } #[test] fn test_serialize() { let mut buf = packet().serialize().try_into_mut().unwrap(); - assert_eq!(buf.len(), 1060); + assert_eq!(buf.len(), 1070); // Base header - assert_eq!(buf.get_u8(), 0x8); // Version 0, final flag set - assert_eq!(buf.get_u8(), 6); // Extension words + assert_eq!(buf.get_u8(), 0xC); // Version 0, final, extension + assert_eq!(buf.get_u8(), 0); // Reserved assert_eq!(buf.get_u16(), 0x8811); // Track handle assert_eq!(buf.get_u16(), 0x4422); // Sequence assert_eq!(buf.get_u16(), 0x4411); // Frame number assert_eq!(buf.get_u32(), 0x44221188); // Timestamp + assert_eq!(buf.get_u16(), 8); // Extension words // E2EE extension - assert_eq!(buf.get_u8(), 0x1C); // ID 1, length 12 + assert_eq!(buf.get_u16(), 1); // ID 1, + assert_eq!(buf.get_u16(), 12); // Length 12 assert_eq!(buf.get_u8(), 0xFA); // Key index assert_eq!(buf.copy_to_bytes(12), vec![0x3C; 12]); // User timestamp extension - assert_eq!(buf.get_u8(), 0x27); // ID 2, length 7 + assert_eq!(buf.get_u16(), 2); // ID 2 + assert_eq!(buf.get_u16(), 7); // Length 7 assert_eq!(buf.get_u64(), 0x4411221111118811); - assert_eq!(buf.get_u8(), 0); // Padding + assert_eq!(buf.copy_to_bytes(3), vec![0; 3]); // Padding assert_eq!(buf.copy_to_bytes(1024), vec![0xFA; 1024]); // Payload assert_eq!(buf.remaining(), 0); From 547e94c4d28ab9dc651466b51d973e956a65a16f Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 10 Jan 2026 15:50:20 +1100 Subject: [PATCH 114/232] Prototype depacketizer --- livekit-datatrack/src/remote/depacketizer.rs | 21 ++++++++++++---- livekit-datatrack/src/remote/pipeline.rs | 26 ++++++++++++-------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/livekit-datatrack/src/remote/depacketizer.rs b/livekit-datatrack/src/remote/depacketizer.rs index 63438cfaf..b424977e4 100644 --- a/livekit-datatrack/src/remote/depacketizer.rs +++ b/livekit-datatrack/src/remote/depacketizer.rs @@ -12,11 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{dtp::Dtp, frame::DataTrackFrame}; +use crate::dtp::{Dtp, Extensions, FrameMarker}; +use bytes::Bytes; +/// Converts packets into application-level frames. pub struct Depacketizer; -// TODO: implement depacketizer +/// Output of [`Depacketizer`]. +pub struct DepacketizerFrame { + pub payload: Bytes, + pub extensions: Extensions, +} impl Depacketizer { /// Creates a new depacketizer. @@ -25,7 +31,12 @@ impl Depacketizer { } /// Push a packet into the depacketizer, returning a complete frame if one is available. - pub fn push(&mut self, dtp: Dtp) -> Option { - todo!() + pub fn push(&mut self, dtp: Dtp) -> Option { + match dtp.header.marker { + FrameMarker::Single => { + DepacketizerFrame { payload: dtp.payload, extensions: dtp.header.extensions }.into() + } + _ => unimplemented!("Multi-packet frames not supported yet"), + } } -} \ No newline at end of file +} diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index d1a1d9c71..391216243 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -17,6 +17,7 @@ use crate::{ api::{DataTrackFrame, DataTrackInfo}, dtp::Dtp, e2ee::{DecryptionProvider, EncryptedPayload}, + remote::depacketizer::DepacketizerFrame, }; use std::sync::Arc; use tokio::sync::{broadcast, mpsc, watch}; @@ -49,19 +50,17 @@ impl RemoteTrackTask { // TODO: send unsubscribe if needed } - async fn receive_packet(&mut self, mut dtp: Dtp) { + fn receive_packet(&mut self, dtp: Dtp) { + let Some(mut frame) = self.depacketizer.push(dtp) else { return }; if let Some(decryption) = &self.decryption { debug_assert!(self.info.uses_e2ee); - let Some(e2ee) = dtp.header.extensions.e2ee else { + let Some(e2ee) = frame.extensions.e2ee else { log::error!("Missing E2EE meta"); return; }; - let encrypted_payload = EncryptedPayload { - payload: dtp.payload, - iv: e2ee.iv, - key_index: e2ee.key_index, - }; + let encrypted_payload = + EncryptedPayload { payload: frame.payload, iv: e2ee.iv, key_index: e2ee.key_index }; let decrypted_payload = match decryption.decrypt(encrypted_payload) { Ok(decrypted_payload) => decrypted_payload, Err(err) => { @@ -69,10 +68,17 @@ impl RemoteTrackTask { return; } }; - dtp.payload = decrypted_payload; + frame.payload = decrypted_payload; } - if let Some(frame) = self.depacketizer.push(dtp) { - _ = self.frame_tx.send(frame); + _ = self.frame_tx.send(frame.into()); + } +} + +impl From for DataTrackFrame { + fn from(frame: DepacketizerFrame) -> Self { + Self { + payload: frame.payload, + user_timestamp: frame.extensions.user_timestamp.map(|v| v.0), } } } From 851d07786ff12405e8ea6abc23b1fe87b6be5845 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 10 Jan 2026 15:52:12 +1100 Subject: [PATCH 115/232] Doc --- livekit-datatrack/src/local/pipeline.rs | 2 +- livekit-datatrack/src/remote/pipeline.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 1447d02f2..8c46c5d33 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -22,7 +22,7 @@ use crate::{ use std::sync::Arc; use tokio::sync::{mpsc, watch}; -/// Task responsible for operating an individual published data track. +/// Task responsible for publishing frames for an individual data track. pub(super) struct LocalTrackTask { pub packetizer: Packetizer, pub encryption: Option>, diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 391216243..28c939452 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -22,6 +22,7 @@ use crate::{ use std::sync::Arc; use tokio::sync::{broadcast, mpsc, watch}; +/// Task responsible for receiving frames for a subscribed data track. pub(super) struct RemoteTrackTask { pub depacketizer: Depacketizer, pub decryption: Option>, From 5ef58e006fc38b2928232b8fa100ad22364f4280 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 10 Jan 2026 16:25:37 +1100 Subject: [PATCH 116/232] Update data track e2e test --- livekit/tests/data_track_test.rs | 60 ++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 7168ed2d4..14da6d4d9 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -16,7 +16,10 @@ use { anyhow::{Ok, Result}, common::test_rooms, - livekit::data_track::{schema, Mime}, + futures_util::StreamExt, + livekit::{data_track::DataTrackOptions, RoomEvent}, + std::time::Duration, + tokio::{time::timeout, try_join}, }; mod common; @@ -24,25 +27,52 @@ mod common; #[cfg(feature = "__lk-e2e-test")] #[test_log::test(tokio::test)] async fn test_data_track() -> Result<()> { - use livekit::data_track::DataTrackOptions; + let mut rooms = test_rooms(2).await?; + let (pub_room, _) = rooms.pop().unwrap(); + let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); + let pub_identity = pub_room.local_participant().identity(); - let (room, mut event_rx) = test_rooms(1).await?.pop().unwrap(); + const FRAME_COUNT: usize = 16; + const FRAME_PAYLOAD: &[u8] = &[0xFA; 256]; - let options = DataTrackOptions::with_name("led_color") - .mime(Mime::JSON) - .disable_e2ee(false); + let publish_track = async move { + let track = pub_room + .local_participant() + .publish_data_track(DataTrackOptions::with_name("my_track")) + .await?; - let track = room.local_participant().publish_data_track(options).await?; - for idx in 1..25 { - // track.publish() - } + assert!(track.is_published()); + assert!(track.info().sid().starts_with("DTR_")); + assert!(!track.info().uses_e2ee()); + assert_eq!(track.info().name(), "my_track"); - while let Some(event) = event_rx.recv().await { - use livekit::RoomEvent; - let RoomEvent::TrackPublished { publication, participant } = event else { continue }; - - } + for _ in 0..FRAME_COUNT { + track.publish(FRAME_PAYLOAD.into())?; + } + Ok(()) + }; + let subscribe_to_track = async move { + while let Some(event) = sub_room_event_rx.recv().await { + let RoomEvent::RemoteDataTrackPublished(track) = event else { + continue; + }; + assert!(track.is_published()); + assert!(track.info().sid().starts_with("DTR_")); + assert!(!track.info().uses_e2ee()); + assert_eq!(track.info().name(), "my_track"); + assert_eq!(track.publisher_identity(), pub_identity.as_str()); + let mut frame_stream = track.subscribe().await?; + while let Some(frame) = frame_stream.next().await { + assert_eq!(frame.payload(), FRAME_PAYLOAD); + assert_eq!(frame.user_timestamp(), None); + } + break; + } + Ok(()) + }; + timeout(Duration::from_secs(5), async { try_join!(publish_track, subscribe_to_track) }) + .await??; Ok(()) } From 5c15a3a36e07a0d7330e691835e55dd2e7e7df7c Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 10 Jan 2026 16:33:56 +1100 Subject: [PATCH 117/232] Update data channel label --- livekit/src/rtc_engine/rtc_session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 769ebf0e6..7f88a7dab 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -69,7 +69,7 @@ pub const ICE_CONNECT_TIMEOUT: Duration = Duration::from_secs(15); pub const TRACK_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); pub const LOSSY_DC_LABEL: &str = "_lossy"; pub const RELIABLE_DC_LABEL: &str = "_reliable"; -pub const DATA_TRACK_DC_LABEL: &str = "_datatrack"; +pub const DATA_TRACK_DC_LABEL: &str = "_data_track"; pub const RELIABLE_RECEIVED_STATE_TTL: Duration = Duration::from_secs(30); pub const PUBLISHER_NEGOTIATION_FREQUENCY: Duration = Duration::from_millis(150); pub const INITIAL_BUFFERED_AMOUNT_LOW_THRESHOLD: u64 = 2 * 1024 * 1024; From b4e662fffb1eeae215a254ce5f0b205dcef848d5 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 10 Jan 2026 16:44:20 +1100 Subject: [PATCH 118/232] Share options --- livekit/src/rtc_engine/rtc_session.rs | 29 ++++++++++----------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 7f88a7dab..6ee7b1b76 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -460,28 +460,21 @@ impl RtcSession { proto::SignalTarget::Subscriber, ); - let mut lossy_dc = publisher_pc.peer_connection().create_data_channel( - LOSSY_DC_LABEL, - DataChannelInit { - ordered: false, - max_retransmits: Some(0), - ..DataChannelInit::default() - }, - )?; - let mut reliable_dc = publisher_pc.peer_connection().create_data_channel( RELIABLE_DC_LABEL, - DataChannelInit { ordered: true, ..DataChannelInit::default() }, + DataChannelInit { ordered: true, ..Default::default() }, )?; - let mut dt_transport = publisher_pc.peer_connection().create_data_channel( - DATA_TRACK_DC_LABEL, - DataChannelInit { - ordered: false, - max_retransmits: Some(0), - ..DataChannelInit::default() - }, - )?; + let lossy_options = + DataChannelInit { ordered: false, max_retransmits: Some(0), ..Default::default() }; + + let mut lossy_dc = publisher_pc + .peer_connection() + .create_data_channel(LOSSY_DC_LABEL, lossy_options.clone())?; + + let mut dt_transport = publisher_pc + .peer_connection() + .create_data_channel(DATA_TRACK_DC_LABEL, lossy_options)?; // Forward events received inside the signaling thread to our rtc channel rtc_events::forward_pc_events(&mut publisher_pc, rtc_emitter.clone()); From 2b586b2622f395e9dfe4560ddec1a76eda27703a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:02:28 +1100 Subject: [PATCH 119/232] Store reference to data track dc --- livekit/src/rtc_engine/rtc_session.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 6ee7b1b76..ec564ae29 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -365,6 +365,7 @@ struct SessionInner { // so we can receive data from other participants sub_lossy_dc: Mutex>, sub_reliable_dc: Mutex>, + sub_dt_transport: Mutex>, closed: AtomicBool, emitter: SessionEmitter, @@ -528,6 +529,7 @@ impl RtcSession { dc_emitter, sub_lossy_dc: Mutex::new(None), sub_reliable_dc: Mutex::new(None), + sub_dt_transport: Mutex::new(None), closed: Default::default(), emitter, options, @@ -1196,13 +1198,16 @@ impl SessionInner { } RtcEvent::DataChannel { data_channel, target } => { log::debug!("received data channel: {:?} {:?}", data_channel, target); - if target == SignalTarget::Subscriber { - if data_channel.label() == LOSSY_DC_LABEL { - self.sub_lossy_dc.lock().replace(data_channel); - } else if data_channel.label() == RELIABLE_DC_LABEL { - self.sub_reliable_dc.lock().replace(data_channel); - } + if target != SignalTarget::Subscriber { + return Ok(()); } + let dc_ref = match data_channel.label().as_str() { + LOSSY_DC_LABEL => &self.sub_lossy_dc, + RELIABLE_DC_LABEL => &self.sub_reliable_dc, + DATA_TRACK_DC_LABEL => &self.sub_dt_transport, + _ => return Ok(()), + }; + dc_ref.lock().replace(data_channel); } RtcEvent::Offer { offer, target: _ } => { // Send the publisher offer to the server From 90d145500e26708d2dc64734b2cd8578ce913e34 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:28:04 +1100 Subject: [PATCH 120/232] Ensure publisher transport connected --- livekit/src/rtc_engine/rtc_session.rs | 28 +++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index ec564ae29..53e427c31 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -1636,9 +1636,13 @@ impl SessionInner { } pub async fn publish_data_track( - &self, + self: &Arc, options: DataTrackOptions, ) -> Result { + self.ensure_publisher_connected_with_dc(self.dt_transport.clone()) + .await + .inspect_err(|err| log::debug!("Data track transport not connected: {}", err)) + .map_err(|_| PublishError::Disconnected)?; // TODO: better error mapping self.local_dt_manager.publish_track(options).await } @@ -1894,11 +1898,23 @@ impl SessionInner { } } - /// Ensure the Publisher PC is connected, if not, start the negotiation - /// This is required when sending data to the server + /// Ensure the publisher peer connection and data channel for the specified packet + /// type are connected. If not, start the negotiation. + /// + /// This is required when sending data to the server. + /// async fn ensure_publisher_connected( self: &Arc, kind: DataPacketKind, + ) -> EngineResult<()> { + let required_dc = self.data_channel(SignalTarget::Publisher, kind).unwrap(); + self.ensure_publisher_connected_with_dc(required_dc).await?; + Ok(()) + } + + async fn ensure_publisher_connected_with_dc( + self: &Arc, + required_dc: DataChannel, ) -> EngineResult<()> { if !self.has_published.load(Ordering::Acquire) { // The publisher has never been connected, start the negotiation @@ -1906,14 +1922,14 @@ impl SessionInner { self.publisher_negotiation_needed(); } - let dc = self.data_channel(SignalTarget::Publisher, kind).unwrap(); - if dc.state() == DataChannelState::Open { + if required_dc.state() == DataChannelState::Open { return Ok(()); } // Wait until the PeerConnection is connected let wait_connected = async { - while !self.publisher_pc.is_connected() || dc.state() != DataChannelState::Open { + while !self.publisher_pc.is_connected() || required_dc.state() != DataChannelState::Open + { if self.closed.load(Ordering::Acquire) { return Err(EngineError::Connection("closed".into())); } From bbff3994d246de334d16420fe1f3c3639d42a294 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:33:30 +1100 Subject: [PATCH 121/232] Log --- examples/data_track/src/subscriber.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/data_track/src/subscriber.rs b/examples/data_track/src/subscriber.rs index f3669e64e..159f09607 100644 --- a/examples/data_track/src/subscriber.rs +++ b/examples/data_track/src/subscriber.rs @@ -22,6 +22,7 @@ async fn main() -> Result<()> { /// Waits for the first data track to be published and returns it. async fn wait_for_publication(mut rx: UnboundedReceiver) -> Option { + log::info!("Waiting for publication…"); while let Some(event) = rx.recv().await { match event { RoomEvent::RemoteDataTrackPublished(track) => return Some(track), From 72d68d95831b27523b2500a493da666f60b8dffd Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:55:37 +1100 Subject: [PATCH 122/232] Do not process as standard data packet --- livekit/src/rtc_engine/rtc_events.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/livekit/src/rtc_engine/rtc_events.rs b/livekit/src/rtc_engine/rtc_events.rs index d5f59f794..af6bc0f46 100644 --- a/livekit/src/rtc_engine/rtc_events.rs +++ b/livekit/src/rtc_engine/rtc_events.rs @@ -18,7 +18,10 @@ use tokio::sync::mpsc; use super::peer_transport::PeerTransport; use crate::{ - rtc_engine::{peer_transport::OnOfferCreated, rtc_session::RELIABLE_DC_LABEL}, + rtc_engine::{ + peer_transport::OnOfferCreated, + rtc_session::{LOSSY_DC_LABEL, RELIABLE_DC_LABEL}, + }, DataPacketKind, }; @@ -94,13 +97,11 @@ fn on_data_channel( emitter: RtcEmitter, ) -> rtc::peer_connection::OnDataChannel { Box::new(move |data_channel| { - let kind = if data_channel.label() == RELIABLE_DC_LABEL { - DataPacketKind::Reliable - } else { - DataPacketKind::Lossy - }; - data_channel.on_message(Some(on_message(emitter.clone(), kind))); - + match data_channel.label().as_str() { + RELIABLE_DC_LABEL => data_channel.on_message(Some(on_message(emitter.clone(), DataPacketKind::Reliable))), + LOSSY_DC_LABEL => data_channel.on_message(Some(on_message(emitter.clone(), DataPacketKind::Lossy))), + _ => {} + } let _ = emitter.send(RtcEvent::DataChannel { data_channel, target }); }) } From e1cd4d3fec9a70dade9aa34fefda10fbab862c72 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:55:24 +1100 Subject: [PATCH 123/232] Debug logging --- livekit-datatrack/src/local/manager.rs | 3 +++ livekit-datatrack/src/local/pipeline.rs | 2 ++ livekit-datatrack/src/remote/manager.rs | 5 +++++ livekit-datatrack/src/remote/pipeline.rs | 2 ++ 4 files changed, 12 insertions(+) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 777fb51c6..772be790f 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -203,13 +203,16 @@ pub struct ManagerTask { impl ManagerTask { pub async fn run(mut self) { + log::debug!("Task started"); while let Some(event) = self.event_in_rx.recv().await { + log::debug!("Input event: {:?}", event); if matches!(event, InputEvent::Shutdown) { break; } self.handle_event(event); } self.shutdown().await; + log::debug!("Task ended"); } fn handle_event(&mut self, event: InputEvent) { diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 8c46c5d33..7410643bd 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -34,6 +34,7 @@ pub(super) struct LocalTrackTask { impl LocalTrackTask { pub async fn run(mut self) { + log::debug!("Task started: sid={}", self.info.sid); let mut state = *self.state_rx.borrow(); while state.is_published() { tokio::select! { @@ -51,6 +52,7 @@ impl LocalTrackTask { let event = UnpublishRequestEvent { handle: self.info.handle }; _ = self.event_out_tx.try_send(event.into()); } + log::debug!("Task ended: sid={}", self.info.sid); } fn publish_frame(&mut self, mut frame: DataTrackFrame) { diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 18cb47e39..8f9e77156 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -181,7 +181,9 @@ pub struct ManagerTask { impl ManagerTask { pub async fn run(mut self) { + log::debug!("Task started"); while let Some(event) = self.event_in_rx.recv().await { + log::debug!("Input event: {:?}", event); match event { InputEvent::PublicationsUpdated(event) => { self.handle_publications_updated(event).await @@ -193,6 +195,7 @@ impl ManagerTask { } } self.shutdown().await; + log::debug!("Task ended"); } async fn handle_publications_updated(&mut self, event: PublicationsUpdatedEvent) { @@ -218,6 +221,7 @@ impl ManagerTask { } async fn handle_track_published(&mut self, publisher_identity: String, info: DataTrackInfo) { + log::debug!("Track published: sid={}", info.sid); if self.descriptors.contains_key(&info.sid) { log::error!("Existing descriptor for track {}", info.sid); return; @@ -239,6 +243,7 @@ impl ManagerTask { } fn handle_track_unpublished(&mut self, sid: DataTrackSid) { + log::debug!("Track unpublished: sid={}", sid); self.sub_handles.remove(&sid); let Some(descriptor) = self.descriptors.remove(&sid) else { log::error!("Unknown track {}", sid); diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 28c939452..6c066fae8 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -35,6 +35,7 @@ pub(super) struct RemoteTrackTask { impl RemoteTrackTask { pub async fn run(mut self) { + log::debug!("Task started: sid={}", self.info.sid); let mut state = *self.state_rx.borrow(); while state.is_published() { tokio::select! { @@ -48,6 +49,7 @@ impl RemoteTrackTask { else => break } } + log::debug!("Task ended: sid={}", self.info.sid); // TODO: send unsubscribe if needed } From 2af07144b9fbbc3568a55510f494eb503bc787d6 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:55:42 +1100 Subject: [PATCH 124/232] Disable auto subscribe in example --- examples/data_track/src/subscriber.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/data_track/src/subscriber.rs b/examples/data_track/src/subscriber.rs index 159f09607..771002d76 100644 --- a/examples/data_track/src/subscriber.rs +++ b/examples/data_track/src/subscriber.rs @@ -11,7 +11,9 @@ async fn main() -> Result<()> { let url = env::var("LIVEKIT_URL").expect("LIVEKIT_URL is not set"); let token = env::var("LIVEKIT_TOKEN").expect("LIVEKIT_TOKEN is not set"); - let (_, rx) = Room::connect(&url, &token, RoomOptions::default()).await?; + let mut options = RoomOptions::default(); + options.auto_subscribe = false; + let (_, rx) = Room::connect(&url, &token, options).await?; tokio::select! { Some(track) = wait_for_publication(rx) => subscribe(track).await?, From caf8919b5dd8ab8eb668a0c556604cc5ffe11012 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 09:03:19 +1100 Subject: [PATCH 125/232] Don't expose local participant's tracks to remote manager --- livekit-datatrack/src/remote/proto.rs | 15 +++++++++++++-- livekit/src/rtc_engine/rtc_session.rs | 3 ++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index b0d714ad4..88666ce17 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -47,7 +47,7 @@ impl TryFrom for SubscriberHandlesEvent { pub fn event_from_join( msg: &mut proto::JoinResponse, ) -> Result { - event_from_participant_info(&mut msg.other_participants) + event_from_participant_info(&mut msg.other_participants, None) } /// Extracts a [`PublicationsUpdatedEvent`] from a participant update. @@ -57,15 +57,26 @@ pub fn event_from_join( /// pub fn event_from_participant_update( msg: &mut proto::ParticipantUpdate, + local_participant_identity: &str, ) -> Result { - event_from_participant_info(&mut msg.participants) + // TODO: is there a better way to exclude the local participant? + event_from_participant_info(&mut msg.participants, local_participant_identity.into()) } +/// Extracts a [`PublicationsUpdatedEvent`] from a participant info list. +/// +/// Tracks published by the local participant will be filtered out if the local +/// participant identity is set. +/// fn event_from_participant_info( msg: &mut Vec, + local_participant_identity: Option<&str>, ) -> Result { let tracks_by_participant = msg .iter_mut() + .filter(|participant| { + local_participant_identity.map_or(true, |identity| participant.identity != identity) + }) .map(|participant| -> Result<_, InternalError> { Ok((participant.identity.clone(), extract_track_info(participant)?)) }) diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 53e427c31..111426f59 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -1099,7 +1099,8 @@ impl SessionInner { ); } proto::signal_response::Message::Update(mut update) => { - if let Ok(event) = dt::remote::event_from_participant_update(&mut update) { + let local_participant_identity = self.participant_info.identity.as_str().into(); + if let Ok(event) = dt::remote::event_from_participant_update(&mut update, local_participant_identity) { _ = self.remote_dt_manager.send(event.into()); } let _ = self From 2e6e7d2bb147297c8a3dab1baac68764516f7f29 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 09:06:18 +1100 Subject: [PATCH 126/232] Doc --- livekit-datatrack/src/error.rs | 5 +++++ livekit-datatrack/src/lib.rs | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/livekit-datatrack/src/error.rs b/livekit-datatrack/src/error.rs index b6e4a36e0..521b8a92a 100644 --- a/livekit-datatrack/src/error.rs +++ b/livekit-datatrack/src/error.rs @@ -14,6 +14,11 @@ use thiserror::Error; +/// Internal data track error. +/// +/// Occurrences of this error type are unexpected and likely indicate +/// a bug. If encountered, please report on GitHub and include the full error description. +/// #[derive(Debug, Error)] #[error(transparent)] pub struct InternalError(#[from] anyhow::Error); diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index 5877749b0..a18f1d3f8 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -21,16 +21,16 @@ mod local; /// Remote track subscription. mod remote; -/// Frame and frame builder. +/// Application-level frame. mod frame; -/// End-to-end encryption. +/// Provider for end-to-end encryption/decryption. mod e2ee; -/// Data track packet format. +/// Data track packet (DTP) format. mod dtp; -/// Common utilities. +/// Internal utilities. mod utils; /// Internal error. From 6776e45d75f33b624e3cbc12581796e8b18ca468 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 09:28:11 +1100 Subject: [PATCH 127/232] Rename for clarity --- livekit-datatrack/src/remote/manager.rs | 29 +++++++++++++------------ livekit-datatrack/src/remote/proto.rs | 12 +++++----- livekit/src/rtc_engine/rtc_session.rs | 4 ++-- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 8f9e77156..6aa7f440c 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -34,7 +34,7 @@ use tokio_stream::{wrappers::ReceiverStream, Stream}; /// An external event handled by [`Manager`]. #[derive(Debug, FromVariants)] pub enum InputEvent { - PublicationsUpdated(PublicationsUpdatedEvent), + PublicationUpdates(PublicationUpdatesEvent), Subscribe(SubscribeEvent), SubscriberHandles(SubscriberHandlesEvent), /// Packet has been received over the transport. @@ -52,15 +52,15 @@ pub enum OutputEvent { TrackAvailable(RemoteDataTrack), } -/// Track publications updated for a specific participant. +/// Track publications by remote participants updated. /// /// This is used to detect newly published tracks as well as /// tracks that have been unpublished. /// #[derive(Debug)] -pub struct PublicationsUpdatedEvent { +pub struct PublicationUpdatesEvent { /// Mapping between participant identity and data tracks published by that participant. - pub tracks_by_participant: HashMap>, + pub updates: HashMap>, } /// Subscriber handles available or updated. @@ -185,8 +185,8 @@ impl ManagerTask { while let Some(event) = self.event_in_rx.recv().await { log::debug!("Input event: {:?}", event); match event { - InputEvent::PublicationsUpdated(event) => { - self.handle_publications_updated(event).await + InputEvent::PublicationUpdates(event) => { + self.handle_publication_updates(event).await } InputEvent::Subscribe(event) => self.handle_subscribe(event).await, InputEvent::SubscriberHandles(event) => self.handle_subscriber_handles(event), @@ -198,11 +198,14 @@ impl ManagerTask { log::debug!("Task ended"); } - async fn handle_publications_updated(&mut self, event: PublicationsUpdatedEvent) { + async fn handle_publication_updates(&mut self, event: PublicationUpdatesEvent) { + if event.updates.is_empty() { + return; + } let mut sids_in_update = HashSet::new(); // Detect published track - for (publisher_identity, tracks) in event.tracks_by_participant { + for (publisher_identity, tracks) in event.updates { for info in tracks { sids_in_update.insert(info.sid.clone()); if self.descriptors.contains_key(&info.sid) { @@ -262,10 +265,8 @@ impl ManagerTask { }; match &mut descriptor.state { DescriptorState::Available => { - let update_event = SubscriptionUpdatedEvent { - sid: event.sid.clone(), - subscribe: true, - }; + let update_event = + SubscriptionUpdatedEvent { sid: event.sid.clone(), subscribe: true }; _ = self.event_out_tx.send(update_event.into()).await; descriptor.state = DescriptorState::PendingSubscriberHandle { result_txs: vec![event.result_tx] }; @@ -394,8 +395,8 @@ mod tests { livekit_runtime::spawn(manager_task.run()); // Simulate track published - let event = PublicationsUpdatedEvent { - tracks_by_participant: HashMap::from([( + let event = PublicationUpdatesEvent { + updates: HashMap::from([( publisher_identity.clone(), vec![DataTrackInfo { sid: "DTR_1234".to_string().try_into().unwrap(), diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index 88666ce17..7e529f55a 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::manager::{PublicationsUpdatedEvent, SubscriberHandlesEvent, SubscriptionUpdatedEvent}; +use super::manager::{PublicationUpdatesEvent, SubscriberHandlesEvent, SubscriptionUpdatedEvent}; use crate::{ api::{DataTrackInfo, DataTrackSid, InternalError}, dtp::Handle, @@ -46,7 +46,7 @@ impl TryFrom for SubscriberHandlesEvent { /// pub fn event_from_join( msg: &mut proto::JoinResponse, -) -> Result { +) -> Result { event_from_participant_info(&mut msg.other_participants, None) } @@ -58,7 +58,7 @@ pub fn event_from_join( pub fn event_from_participant_update( msg: &mut proto::ParticipantUpdate, local_participant_identity: &str, -) -> Result { +) -> Result { // TODO: is there a better way to exclude the local participant? event_from_participant_info(&mut msg.participants, local_participant_identity.into()) } @@ -71,8 +71,8 @@ pub fn event_from_participant_update( fn event_from_participant_info( msg: &mut Vec, local_participant_identity: Option<&str>, -) -> Result { - let tracks_by_participant = msg +) -> Result { + let updates = msg .iter_mut() .filter(|participant| { local_participant_identity.map_or(true, |identity| participant.identity != identity) @@ -81,7 +81,7 @@ fn event_from_participant_info( Ok((participant.identity.clone(), extract_track_info(participant)?)) }) .collect::>, _>>()?; - Ok(PublicationsUpdatedEvent { tracks_by_participant }) + Ok(PublicationUpdatesEvent { updates }) } fn extract_track_info(msg: &mut ParticipantInfo) -> Result, InternalError> { diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 111426f59..9ef7b03cd 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -501,8 +501,8 @@ impl RtcSession { let (remote_dt_manager, remote_dt_task, remote_dt_events) = dt::remote::Manager::new(remote_dt_options); forward_incoming_dt_packets(&mut dt_transport, remote_dt_manager.clone()); - if let Ok(publications_updated) = dt::remote::event_from_join(&mut join_response) { - _ = remote_dt_manager.send(publications_updated.into()); + if let Ok(initial_publications) = dt::remote::event_from_join(&mut join_response) { + _ = remote_dt_manager.send(initial_publications.into()); } let (close_tx, close_rx) = watch::channel(false); From f76aeffbec3a3afba917e6617298e225e026ca90 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 09:41:01 +1100 Subject: [PATCH 128/232] Forward incoming packets to remote manager --- livekit/src/rtc_engine/rtc_session.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 9ef7b03cd..b77108ed0 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -500,7 +500,6 @@ impl RtcSession { }; let (remote_dt_manager, remote_dt_task, remote_dt_events) = dt::remote::Manager::new(remote_dt_options); - forward_incoming_dt_packets(&mut dt_transport, remote_dt_manager.clone()); if let Ok(initial_publications) = dt::remote::event_from_join(&mut join_response) { _ = remote_dt_manager.send(initial_publications.into()); } @@ -1205,7 +1204,10 @@ impl SessionInner { let dc_ref = match data_channel.label().as_str() { LOSSY_DC_LABEL => &self.sub_lossy_dc, RELIABLE_DC_LABEL => &self.sub_reliable_dc, - DATA_TRACK_DC_LABEL => &self.sub_dt_transport, + DATA_TRACK_DC_LABEL => { + handle_remote_dt_packets(&data_channel, self.remote_dt_manager.clone()); + &self.sub_dt_transport + }, _ => return Ok(()), }; dc_ref.lock().replace(data_channel); @@ -1987,8 +1989,8 @@ impl SessionInner { } } -/// Sets up forwarding of incoming data track packets to the manager as input events. -pub fn forward_incoming_dt_packets(dc: &mut DataChannel, manager: dt::remote::Manager) { +/// Forward remote data track packets to the remote track manager. +pub fn handle_remote_dt_packets(dc: &DataChannel, manager: dt::remote::Manager) { let on_message: libwebrtc::data_channel::OnMessage = Box::new(move |buffer: DataBuffer| { if !buffer.binary { log::error!("Received non-binary message"); From 5a64a47c01f76a35a65afd73e18b090038aaab84 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:09:09 +1100 Subject: [PATCH 129/232] Fix send packet to track task --- livekit-datatrack/src/remote/manager.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 6aa7f440c..5f476f607 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -347,7 +347,9 @@ impl ManagerTask { log::warn!("Received packet for track {} without subscription", descriptor.info.sid); return; }; - _ = packet_tx.send(dtp); + _ = packet_tx + .try_send(dtp) + .inspect_err(|_| log::warn!("Track task saturated, dropping packet")); } /// Performs cleanup before the task ends. From 55aa33e37cdaa73412916813f1614548d3437107 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:09:36 +1100 Subject: [PATCH 130/232] Properly handle ctrl+c --- examples/data_track/src/subscriber.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/data_track/src/subscriber.rs b/examples/data_track/src/subscriber.rs index 771002d76..95517bff5 100644 --- a/examples/data_track/src/subscriber.rs +++ b/examples/data_track/src/subscriber.rs @@ -16,22 +16,22 @@ async fn main() -> Result<()> { let (_, rx) = Room::connect(&url, &token, options).await?; tokio::select! { - Some(track) = wait_for_publication(rx) => subscribe(track).await?, + _ = handle_first_publication(rx) => {} _ = signal::ctrl_c() => {} } Ok(()) } -/// Waits for the first data track to be published and returns it. -async fn wait_for_publication(mut rx: UnboundedReceiver) -> Option { - log::info!("Waiting for publication…"); +/// Subscribe to the first data track published. +async fn handle_first_publication(mut rx: UnboundedReceiver) -> Result<()> { while let Some(event) = rx.recv().await { + log::info!("Waiting for publication…"); match event { - RoomEvent::RemoteDataTrackPublished(track) => return Some(track), + RoomEvent::RemoteDataTrackPublished(track) => subscribe(track).await?, _ => continue, } } - None + Ok(()) } /// Subscribes to the given data track and logs received frames. @@ -45,5 +45,6 @@ async fn subscribe(track: RemoteDataTrack) -> Result<()> { while let Some(frame) = frame_steam.next().await { log::info!("Received {} bytes", frame.payload().len()); } + log::info!("Unsubscribed"); Ok(()) } From f3070e06fbb45b5e3f8f7b7a0102ae1b206374c8 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:12:24 +1100 Subject: [PATCH 131/232] Custom debug impl for frame --- livekit-datatrack/src/frame.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/livekit-datatrack/src/frame.rs b/livekit-datatrack/src/frame.rs index 769e80a5f..c88cbc691 100644 --- a/livekit-datatrack/src/frame.rs +++ b/livekit-datatrack/src/frame.rs @@ -13,9 +13,10 @@ // limitations under the License. use bytes::Bytes; +use core::fmt; /// Application-level frame published to a data track. -#[derive(Debug, Clone, Default)] +#[derive(Clone, Default)] pub struct DataTrackFrame { pub(crate) payload: Bytes, pub(crate) user_timestamp: Option, @@ -46,6 +47,15 @@ impl DataTrackFrame { } } +impl fmt::Debug for DataTrackFrame { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DataTrackFrame") + .field("payload_len", &self.payload.len()) + .field("user_timestamp", &self.user_timestamp) + .finish() + } +} + // MARK: - From implementations impl From for DataTrackFrame { @@ -70,4 +80,4 @@ impl From> for DataTrackFrame { fn from(slice: Box<[u8]>) -> Self { Self { payload: slice.into(), ..Default::default() } } -} \ No newline at end of file +} From 6ec5d5745d558c1e37db816a48ecbd5ff4bff542 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:29:32 +1100 Subject: [PATCH 132/232] Update E2E test --- livekit/tests/data_track_test.rs | 48 ++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 14da6d4d9..ef814844f 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -15,11 +15,11 @@ #[cfg(feature = "__lk-e2e-test")] use { anyhow::{Ok, Result}, - common::test_rooms, + common::test_rooms_with_options, futures_util::StreamExt, - livekit::{data_track::DataTrackOptions, RoomEvent}, - std::time::Duration, - tokio::{time::timeout, try_join}, + livekit::{data_track::DataTrackOptions, RoomEvent, RoomOptions}, + std::{iter, time::Duration}, + tokio::time::{self, timeout}, }; mod common; @@ -27,52 +27,78 @@ mod common; #[cfg(feature = "__lk-e2e-test")] #[test_log::test(tokio::test)] async fn test_data_track() -> Result<()> { - let mut rooms = test_rooms(2).await?; + // Temporary workaround until auto subscribe is disabled on the SFU. + let mut room_options = RoomOptions::default(); + room_options.auto_subscribe = false; + + let mut rooms = test_rooms_with_options(iter::repeat(room_options.clone()).take(2)).await?; + + // let mut rooms = test_rooms(2).await?; + let (pub_room, _) = rooms.pop().unwrap(); let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); let pub_identity = pub_room.local_participant().identity(); - const FRAME_COUNT: usize = 16; + // How many frames to check on the subscriber side. + const FRAME_VERIFY_COUNT: usize = 16; const FRAME_PAYLOAD: &[u8] = &[0xFA; 256]; - let publish_track = async move { + let publish = async move { let track = pub_room .local_participant() .publish_data_track(DataTrackOptions::with_name("my_track")) .await?; + log::info!("Track published: {:?}", track); assert!(track.is_published()); assert!(track.info().sid().starts_with("DTR_")); assert!(!track.info().uses_e2ee()); assert_eq!(track.info().name(), "my_track"); - for _ in 0..FRAME_COUNT { + while track.is_published() { track.publish(FRAME_PAYLOAD.into())?; + time::sleep(Duration::from_millis(25)).await; } Ok(()) }; - let subscribe_to_track = async move { + let subscribe_until_verified = async move { while let Some(event) = sub_room_event_rx.recv().await { let RoomEvent::RemoteDataTrackPublished(track) = event else { continue; }; + log::info!("Got remote track: {:?}", track); + assert!(track.is_published()); assert!(track.info().sid().starts_with("DTR_")); assert!(!track.info().uses_e2ee()); assert_eq!(track.info().name(), "my_track"); assert_eq!(track.publisher_identity(), pub_identity.as_str()); + let mut frame_count = 0; let mut frame_stream = track.subscribe().await?; + while let Some(frame) = frame_stream.next().await { assert_eq!(frame.payload(), FRAME_PAYLOAD); assert_eq!(frame.user_timestamp(), None); + + frame_count += 1; + if frame_count >= FRAME_VERIFY_COUNT { + break; + }; } break; } Ok(()) }; - timeout(Duration::from_secs(5), async { try_join!(publish_track, subscribe_to_track) }) - .await??; + + let verify_frames_received = async { + tokio::select! { + res = publish => res?, + res = subscribe_until_verified => res? + } + Ok(()) + }; + timeout(Duration::from_secs(10), verify_frames_received).await??; Ok(()) } From 28ad0b0d5829196f34099dc0f95c788e7e35210c Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:38:09 +1100 Subject: [PATCH 133/232] Test dev dependencies - Use test-case vs rstest - Introduce fake --- Cargo.lock | 103 +++++++++++----------- livekit-datatrack/Cargo.toml | 3 +- livekit-datatrack/src/dtp/deserialize.rs | 6 +- livekit-datatrack/src/local/manager.rs | 20 +++-- livekit-datatrack/src/local/packetizer.rs | 22 +++-- livekit-datatrack/src/remote/manager.rs | 16 ++-- livekit-datatrack/src/utils/bytes.rs | 8 +- 7 files changed, 95 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f8544e2c..1462576d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1351,6 +1351,12 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + [[package]] name = "digest" version = "0.10.7" @@ -1698,6 +1704,17 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fake" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b0902eb36fbab51c14eda1c186bda119fcff91e5e4e7fc2dd2077298197ce8" +dependencies = [ + "deunicode", + "either", + "rand 0.9.2", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1969,12 +1986,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - [[package]] name = "futures-util" version = "0.3.31" @@ -3077,6 +3088,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", + "fake", "from_variants", "futures-core", "futures-util", @@ -3084,7 +3096,7 @@ dependencies = [ "livekit-runtime", "log", "rand 0.9.2", - "rstest", + "test-case", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -4608,12 +4620,6 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" -[[package]] -name = "relative-path" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" - [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -4703,35 +4709,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "rstest" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" -dependencies = [ - "futures-timer", - "futures-util", - "rstest_macros", -] - -[[package]] -name = "rstest_macros" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" -dependencies = [ - "cfg-if", - "glob", - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version", - "syn 2.0.110", - "unicode-ident", -] - [[package]] name = "rustc-demangle" version = "0.1.26" @@ -4750,15 +4727,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "0.38.44" @@ -5387,6 +5355,39 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", + "test-case-core", +] + [[package]] name = "test-log" version = "0.2.18" diff --git a/livekit-datatrack/Cargo.toml b/livekit-datatrack/Cargo.toml index 9ff49b38f..abdea6f2f 100644 --- a/livekit-datatrack/Cargo.toml +++ b/livekit-datatrack/Cargo.toml @@ -21,4 +21,5 @@ anyhow = "1.0.100" # For internal error handling only rand = "0.9.2" [dev-dependencies] -rstest = "0.26.1" +test-case = "3.3" +fake = "4.4" \ No newline at end of file diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/dtp/deserialize.rs index c404c96a1..2e63e1511 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/dtp/deserialize.rs @@ -138,7 +138,7 @@ impl Extensions { mod tests { use super::*; use bytes::{BufMut, BytesMut}; - use rstest::*; + use test_case::test_matrix; /// Returns the simplest valid packet to use in test. fn valid_packet() -> BytesMut { @@ -205,8 +205,8 @@ mod tests { assert_eq!(dtp.header.extensions.e2ee, None); } - #[rstest] - fn test_ext_skips_padding(#[values(0, 1, 24)] ext_words: usize) { + #[test_matrix([0, 1, 24])] + fn test_ext_skips_padding(ext_words: usize) { let mut raw = valid_packet(); raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 772be790f..6636fc459 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -342,7 +342,7 @@ mod tests { use crate::dtp::Dtp; use futures_util::StreamExt; use livekit_runtime::sleep; - use rstest::*; + use fake::{Fake, faker::lorem::en::Word}; #[tokio::test] async fn test_task_shutdown() { @@ -355,23 +355,25 @@ mod tests { time::timeout(Duration::from_secs(1), join_handle).await.unwrap(); } - #[rstest] - #[case("my_track", 10, 256)] #[tokio::test] - async fn test_publish( - #[case] name: String, - #[case] packet_count: usize, - #[case] payload_size: usize, - ) { + async fn test_publish() { + let payload_size = 256; + let packet_count = 10; + let track_name: String = Word().fake(); + let options = ManagerOptions { encryption: None }; let (manager, manager_task, mut output_events) = Manager::new(options); livekit_runtime::spawn(manager_task.run()); + let track_name_clone = track_name.clone(); let handle_events = async { let mut packets_sent = 0; while let Some(event) = output_events.next().await { match event { OutputEvent::PublishRequest(event) => { + assert!(!event.uses_e2ee); + assert_eq!(event.name, track_name_clone); + // SFU accepts publication let info = DataTrackInfo { sid: "DTR_1234".to_string().try_into().unwrap(), @@ -397,7 +399,7 @@ mod tests { } }; let publish_track = async { - let track_options = DataTrackOptions::with_name(name.clone()); + let track_options = DataTrackOptions::with_name(track_name); let track = manager.publish_track(track_options).await.unwrap(); for _ in 0..packet_count { track.publish(vec![0xFA; payload_size].into()).unwrap(); diff --git a/livekit-datatrack/src/local/packetizer.rs b/livekit-datatrack/src/local/packetizer.rs index 1287fca19..4b9138fbe 100644 --- a/livekit-datatrack/src/local/packetizer.rs +++ b/livekit-datatrack/src/local/packetizer.rs @@ -88,7 +88,7 @@ impl Packetizer { } fn frame_marker(index: usize, packet_count: usize) -> FrameMarker { - if packet_count == 1 { + if packet_count <= 1 { return FrameMarker::Single; } match index { @@ -103,14 +103,18 @@ impl Packetizer { mod tests { use super::*; use crate::dtp::{E2eeExt, UserTimestampExt}; - use rstest::rstest; + use test_case::test_matrix; - #[rstest] - fn test_packetize( - #[values(0, 32, 784)] payload_size: usize, - #[values(256, 1024)] mtu_size: usize, - #[values(true, false)] with_exts: bool, - ) { + #[test] + fn test_frame_marker() { + assert_eq!(Packetizer::frame_marker(0, 1), FrameMarker::Single); + assert_eq!(Packetizer::frame_marker(0, 10), FrameMarker::Start); + assert_eq!(Packetizer::frame_marker(4, 10), FrameMarker::Inter); + assert_eq!(Packetizer::frame_marker(9, 10), FrameMarker::Final); + } + + #[test_matrix([0, 128, 784], [256, 1024], [true, false])] + fn test_packetize(payload_size: usize, mtu_size: usize, with_exts: bool) { let handle = 1u32.try_into().unwrap(); let e2ee = E2eeExt { key_index: 255, iv: [0xCD; 12] }; let user_timestamp = UserTimestampExt(u64::MAX); @@ -132,6 +136,7 @@ mod tests { } for (index, packet) in packets.iter().enumerate() { + assert_eq!(packet.header.marker, Packetizer::frame_marker(index, packets.len())); assert_eq!(packet.header.frame_number, 0); assert_eq!(packet.header.sequence, index as u16); assert_eq!(packet.header.extensions.e2ee, with_exts.then_some(e2ee)); @@ -140,6 +145,5 @@ mod tests { with_exts.then_some(user_timestamp) ); } - assert_eq!(packets.last().unwrap().header.marker, FrameMarker::Final); } } diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 5f476f607..4648b7e83 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -372,8 +372,11 @@ impl ManagerTask { #[cfg(test)] mod tests { use super::*; + use fake::{ + faker::{internet::en::SafeEmail, lorem::en::Word}, + Fake, + }; use futures_util::StreamExt; - use rstest::*; use std::{collections::HashMap, time::Duration}; use tokio::time; @@ -388,10 +391,11 @@ mod tests { time::timeout(Duration::from_secs(1), join_handle).await.unwrap(); } - #[rstest] - #[case("my_track", "some_identity")] #[tokio::test] - async fn test_subscribe(#[case] name: String, #[case] publisher_identity: String) { + async fn test_subscribe() { + let publisher_identity: String = SafeEmail().fake(); + let track_name: String = Word().fake(); + let options = ManagerOptions { decryption: None }; let (manager, manager_task, mut output_events) = Manager::new(options); livekit_runtime::spawn(manager_task.run()); @@ -403,7 +407,7 @@ mod tests { vec![DataTrackInfo { sid: "DTR_1234".to_string().try_into().unwrap(), handle: 1024u32.try_into().unwrap(), - name: name.clone(), + name: track_name.clone(), uses_e2ee: false, }], )]), @@ -422,7 +426,7 @@ mod tests { let track = wait_for_track.await; assert!(track.is_published()); - assert_eq!(track.info().name, name); + assert_eq!(track.info().name, track_name); assert_eq!(track.publisher_identity(), publisher_identity); let simulate_subscriber_handles = async { diff --git a/livekit-datatrack/src/utils/bytes.rs b/livekit-datatrack/src/utils/bytes.rs index 610e6b4fa..8a2aaac32 100644 --- a/livekit-datatrack/src/utils/bytes.rs +++ b/livekit-datatrack/src/utils/bytes.rs @@ -55,7 +55,7 @@ impl Iterator for ChunkIter { #[cfg(test)] mod tests { use super::*; - use rstest::rstest; + use test_case::test_matrix; #[test] fn test_empty_source() { @@ -64,10 +64,10 @@ mod tests { assert!(chunks.is_empty()) } - #[rstest] + #[test_matrix([1, 128, 333], [1, 64, 128, 256, 123])] fn test_chunks( - #[values(1, 128, 333)] chunk_size: usize, - #[values(1, 64, 128, 256, 123)] source_size: usize, + chunk_size: usize, + source_size: usize, ) { let source = Bytes::from(vec![0xCC; source_size]); let chunks: Vec<_> = source.into_chunks(chunk_size).collect(); From a5aec19695098a1f6550d1f08801f92f3ddeb94e Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 17:33:51 +1100 Subject: [PATCH 134/232] Log send error reason --- livekit-datatrack/src/remote/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 4648b7e83..2ca5ec9d9 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -349,7 +349,7 @@ impl ManagerTask { }; _ = packet_tx .try_send(dtp) - .inspect_err(|_| log::warn!("Track task saturated, dropping packet")); + .inspect_err(|err| log::debug!("Cannot send packet to track task: {}", err)); } /// Performs cleanup before the task ends. From 1795c9346d3e3ff5720c3ddee0eec3dc19dda3e5 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 18:29:58 +1100 Subject: [PATCH 135/232] Rename example --- Cargo.lock | 25 ++++++++++--------- .../Cargo.toml | 2 +- .../src/publisher.rs | 0 .../src/subscriber.rs | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) rename examples/{data_track => basic_data_track}/Cargo.toml (94%) rename examples/{data_track => basic_data_track}/src/publisher.rs (100%) rename examples/{data_track => basic_data_track}/src/subscriber.rs (95%) diff --git a/Cargo.lock b/Cargo.lock index 1462576d7..c60bbe58a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -517,6 +517,18 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "basic_data_track" +version = "0.1.0" +dependencies = [ + "anyhow", + "env_logger 0.11.8", + "futures-util", + "livekit", + "log", + "tokio", +] + [[package]] name = "basic_room" version = "0.1.0" @@ -1330,18 +1342,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" -[[package]] -name = "data_track" -version = "0.1.0" -dependencies = [ - "anyhow", - "env_logger 0.11.8", - "futures-util", - "livekit", - "log", - "tokio", -] - [[package]] name = "deranged" version = "0.5.5" @@ -3047,6 +3047,7 @@ dependencies = [ "semver", "serde", "serde_json", + "test-case", "test-log", "thiserror 1.0.69", "tokio", diff --git a/examples/data_track/Cargo.toml b/examples/basic_data_track/Cargo.toml similarity index 94% rename from examples/data_track/Cargo.toml rename to examples/basic_data_track/Cargo.toml index 2bb6022ca..c52a16431 100644 --- a/examples/data_track/Cargo.toml +++ b/examples/basic_data_track/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "data_track" +name = "basic_data_track" version = "0.1.0" edition = "2021" publish = false diff --git a/examples/data_track/src/publisher.rs b/examples/basic_data_track/src/publisher.rs similarity index 100% rename from examples/data_track/src/publisher.rs rename to examples/basic_data_track/src/publisher.rs diff --git a/examples/data_track/src/subscriber.rs b/examples/basic_data_track/src/subscriber.rs similarity index 95% rename from examples/data_track/src/subscriber.rs rename to examples/basic_data_track/src/subscriber.rs index 95517bff5..69252051e 100644 --- a/examples/data_track/src/subscriber.rs +++ b/examples/basic_data_track/src/subscriber.rs @@ -43,7 +43,7 @@ async fn subscribe(track: RemoteDataTrack) -> Result<()> { ); let mut frame_steam = track.subscribe().await?; while let Some(frame) = frame_steam.next().await { - log::info!("Received {} bytes", frame.payload().len()); + log::info!("Received frame ({} bytes)", frame.payload().len()); } log::info!("Unsubscribed"); Ok(()) From 6b06691625ada148db06dfe0b4b42d294a9f0f77 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 18:30:35 +1100 Subject: [PATCH 136/232] Parameterize E2E test --- livekit/tests/data_track_test.rs | 98 ++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index ef814844f..1af43bffe 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -14,19 +14,32 @@ #[cfg(feature = "__lk-e2e-test")] use { - anyhow::{Ok, Result}, + anyhow::{Ok, Result, anyhow}, common::test_rooms_with_options, futures_util::StreamExt, livekit::{data_track::DataTrackOptions, RoomEvent, RoomOptions}, std::{iter, time::Duration}, - tokio::time::{self, timeout}, + test_case::test_case, + tokio::{ + time::{self, timeout}, + try_join, + }, }; mod common; #[cfg(feature = "__lk-e2e-test")] +#[test_case(120., 8_192)] #[test_log::test(tokio::test)] -async fn test_data_track() -> Result<()> { +async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { + + // How long to publish frames for. + const PUBLISH_DURATION: Duration = Duration::from_secs(5); + + // Percentage of total frames that must be received on the subscriber end in + // order for the test to pass. + const MIN_PERCENTAGE: f32 = 0.95; + // Temporary workaround until auto subscribe is disabled on the SFU. let mut room_options = RoomOptions::default(); room_options.auto_subscribe = false; @@ -39,66 +52,67 @@ async fn test_data_track() -> Result<()> { let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); let pub_identity = pub_room.local_participant().identity(); - // How many frames to check on the subscriber side. - const FRAME_VERIFY_COUNT: usize = 16; - const FRAME_PAYLOAD: &[u8] = &[0xFA; 256]; + let frame_count = (PUBLISH_DURATION.as_secs_f64() * publish_fps).round() as u64; + log::info!("Publishing {} frames", frame_count); let publish = async move { let track = pub_room .local_participant() .publish_data_track(DataTrackOptions::with_name("my_track")) .await?; - log::info!("Track published: {:?}", track); + log::info!("Track published"); assert!(track.is_published()); - assert!(track.info().sid().starts_with("DTR_")); assert!(!track.info().uses_e2ee()); assert_eq!(track.info().name(), "my_track"); - while track.is_published() { - track.publish(FRAME_PAYLOAD.into())?; - time::sleep(Duration::from_millis(25)).await; + let sleep_duration = Duration::from_secs_f64(1.0 / publish_fps as f64); + for index in 0..frame_count { + track.publish(vec![index as u8; payload_len].into())?; + time::sleep(sleep_duration).await; } Ok(()) }; - let subscribe_until_verified = async move { - while let Some(event) = sub_room_event_rx.recv().await { - let RoomEvent::RemoteDataTrackPublished(track) = event else { - continue; - }; - log::info!("Got remote track: {:?}", track); - - assert!(track.is_published()); - assert!(track.info().sid().starts_with("DTR_")); - assert!(!track.info().uses_e2ee()); - assert_eq!(track.info().name(), "my_track"); - assert_eq!(track.publisher_identity(), pub_identity.as_str()); - - let mut frame_count = 0; - let mut frame_stream = track.subscribe().await?; - - while let Some(frame) = frame_stream.next().await { - assert_eq!(frame.payload(), FRAME_PAYLOAD); - assert_eq!(frame.user_timestamp(), None); - - frame_count += 1; - if frame_count >= FRAME_VERIFY_COUNT { - break; + let subscribe = async move { + let track = async move { + while let Some(event) = sub_room_event_rx.recv().await { + let RoomEvent::RemoteDataTrackPublished(track) = event else { + continue; }; + return Ok(track) + } + Err(anyhow!("No track published")) + }.await?; + + log::info!("Got remote track: {}", track.info().sid()); + assert!(track.is_published()); + assert!(!track.info().uses_e2ee()); + assert_eq!(track.info().name(), "my_track"); + assert_eq!(track.publisher_identity(), pub_identity.as_str()); + + let mut subscription = track.subscribe().await?; + + let mut recv_count = 0; + + while let Some(frame) = subscription.next().await { + let payload = frame.payload(); + if let Some(first_byte) = payload.first() { + assert!(payload.iter().all(|byte| byte == first_byte)); } - break; + assert_eq!(frame.user_timestamp(), None); + recv_count += 1; } - Ok(()) - }; - let verify_frames_received = async { - tokio::select! { - res = publish => res?, - res = subscribe_until_verified => res? + let recv_percent = recv_count as f32 / frame_count as f32; + log::info!("Received {}/{} frames ({:.2}%)", recv_count, frame_count, recv_percent * 100.); + + if recv_percent < MIN_PERCENTAGE { + Err(anyhow!("Not enough frames received"))?; } Ok(()) }; - timeout(Duration::from_secs(10), verify_frames_received).await??; + timeout(PUBLISH_DURATION + Duration::from_secs(5), async { try_join!(publish, subscribe) }) + .await??; Ok(()) } From f4f14a417ef75d5a1e27731c810ac291491c65b3 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 18:33:07 +1100 Subject: [PATCH 137/232] Update example name in workspace --- Cargo.lock | 1 - Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c60bbe58a..bc82e2a50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3047,7 +3047,6 @@ dependencies = [ "semver", "serde", "serde_json", - "test-case", "test-log", "thiserror 1.0.69", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 06dfe28b7..67a09ae19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ "examples/api", "examples/basic_room", "examples/basic_text_stream", - "examples/data_track", + "examples/basic_data_track", "examples/encrypted_text_stream", "examples/local_audio", "examples/mobile", From b65ab4d26e4aee2c26861593f9012f2309683b5f Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:46:06 +1100 Subject: [PATCH 138/232] Use faker --- Cargo.lock | 55 +++++++++++++++++++++-- livekit-datatrack/Cargo.toml | 2 +- livekit-datatrack/src/dtp/extension.rs | 5 ++- livekit-datatrack/src/dtp/handle.rs | 7 +++ livekit-datatrack/src/dtp/mod.rs | 11 ++--- livekit-datatrack/src/dtp/time.rs | 7 +++ livekit-datatrack/src/local/manager.rs | 19 +++++--- livekit-datatrack/src/local/packetizer.rs | 9 ++-- livekit-datatrack/src/remote/manager.rs | 16 ++++--- livekit-datatrack/src/track.rs | 15 +++++++ 10 files changed, 119 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc82e2a50..6049f3934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,8 +1288,18 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", ] [[package]] @@ -1306,17 +1316,41 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "darling_macro" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "darling_core", + "darling_core 0.14.4", "quote", "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.110", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -1425,6 +1459,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "dummy" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bbcf21279103a67372982cb1156a2154a452451dff2b884cf897ccecce389e0" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "ecolor" version = "0.31.1" @@ -1711,6 +1757,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b0902eb36fbab51c14eda1c186bda119fcff91e5e4e7fc2dd2077298197ce8" dependencies = [ "deunicode", + "dummy", "either", "rand 0.9.2", ] @@ -1871,7 +1918,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55a5e644a80e6d96b2b4910fa7993301d7b7926c045b475b62202b20a36ce69e" dependencies = [ - "darling", + "darling 0.14.4", "proc-macro2", "quote", "syn 1.0.109", diff --git a/livekit-datatrack/Cargo.toml b/livekit-datatrack/Cargo.toml index abdea6f2f..b9419feea 100644 --- a/livekit-datatrack/Cargo.toml +++ b/livekit-datatrack/Cargo.toml @@ -22,4 +22,4 @@ rand = "0.9.2" [dev-dependencies] test-case = "3.3" -fake = "4.4" \ No newline at end of file +fake = { version = "4.4", features = ["derive"] } \ No newline at end of file diff --git a/livekit-datatrack/src/dtp/extension.rs b/livekit-datatrack/src/dtp/extension.rs index b8ffc70ff..e5150445a 100644 --- a/livekit-datatrack/src/dtp/extension.rs +++ b/livekit-datatrack/src/dtp/extension.rs @@ -14,16 +14,19 @@ use core::fmt; -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(test, derive(fake::Dummy))] pub struct Extensions { pub user_timestamp: Option, pub e2ee: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(fake::Dummy))] pub struct UserTimestampExt(pub u64); #[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(fake::Dummy))] pub struct E2eeExt { pub key_index: u8, pub iv: [u8; 12], diff --git a/livekit-datatrack/src/dtp/handle.rs b/livekit-datatrack/src/dtp/handle.rs index 07e66ad7d..c270a92ca 100644 --- a/livekit-datatrack/src/dtp/handle.rs +++ b/livekit-datatrack/src/dtp/handle.rs @@ -80,3 +80,10 @@ impl HandleAllocator { Handle(value).into() } } + +#[cfg(test)] +impl fake::Dummy for Handle { + fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { + Self::try_from(rng.random_range(1..u16::MAX)).unwrap() + } +} \ No newline at end of file diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/dtp/mod.rs index 5ba302349..895983587 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/dtp/mod.rs @@ -15,14 +15,14 @@ use bytes::Bytes; use core::fmt; -mod extension; mod deserialize; +mod extension; mod handle; mod serialize; mod time; -pub use extension::*; pub use deserialize::*; +pub use extension::*; pub use handle::*; pub use serialize::*; pub use time::*; @@ -34,18 +34,19 @@ pub struct Dtp { } #[derive(Debug, Clone)] +#[cfg_attr(test, derive(fake::Dummy))] pub struct Header { pub marker: FrameMarker, pub track_handle: Handle, pub sequence: u16, pub frame_number: u16, pub timestamp: Timestamp<90_000>, - pub extensions: Extensions + pub extensions: Extensions, } - /// Marker indicating a packet's position in relation to a frame. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(fake::Dummy))] pub enum FrameMarker { /// Packet is within a frame. Inter, @@ -54,7 +55,7 @@ pub enum FrameMarker { /// Packet is the first in a frame. Start, /// Packet is the only one in a frame. - Single + Single, } impl fmt::Debug for Dtp { diff --git a/livekit-datatrack/src/dtp/time.rs b/livekit-datatrack/src/dtp/time.rs index 67503fdc0..fdcd88578 100644 --- a/livekit-datatrack/src/dtp/time.rs +++ b/livekit-datatrack/src/dtp/time.rs @@ -114,3 +114,10 @@ mod tests { assert_eq!(clock.at(t1).as_ticks(), clock.at(t0).as_ticks(), "Clock went backwards"); } } + +#[cfg(test)] +impl fake::Dummy for Timestamp { + fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { + Self(rng.random()) + } +} \ No newline at end of file diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 6636fc459..d4425401d 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -339,10 +339,10 @@ impl ManagerTask { #[cfg(test)] mod tests { use super::*; - use crate::dtp::Dtp; + use crate::{api::DataTrackSid, dtp::Dtp}; use futures_util::StreamExt; use livekit_runtime::sleep; - use fake::{Fake, faker::lorem::en::Word}; + use fake::{Fake, Faker, faker::lorem::en::Word}; #[tokio::test] async fn test_task_shutdown() { @@ -359,7 +359,10 @@ mod tests { async fn test_publish() { let payload_size = 256; let packet_count = 10; + let track_name: String = Word().fake(); + let track_sid: DataTrackSid = Faker.fake(); + let handle: Handle = Faker.fake(); let options = ManagerOptions { encryption: None }; let (manager, manager_task, mut output_events) = Manager::new(options); @@ -376,8 +379,8 @@ mod tests { // SFU accepts publication let info = DataTrackInfo { - sid: "DTR_1234".to_string().try_into().unwrap(), - handle: 1u32.try_into().unwrap(), + sid: track_sid.clone(), + handle, name: event.name, uses_e2ee: event.uses_e2ee, }; @@ -391,7 +394,7 @@ mod tests { packets_sent += 1; } OutputEvent::UnpublishRequest(event) => { - assert_eq!(event.handle, 1u32.try_into().unwrap()); + assert_eq!(event.handle, handle); assert_eq!(packets_sent, packet_count); break; } @@ -399,8 +402,12 @@ mod tests { } }; let publish_track = async { - let track_options = DataTrackOptions::with_name(track_name); + let track_options = DataTrackOptions::with_name(track_name.clone()); let track = manager.publish_track(track_options).await.unwrap(); + assert!(!track.info().uses_e2ee()); + assert_eq!(track.info().name(), track_name); + assert_eq!(*track.info().sid(), track_sid); + for _ in 0..packet_count { track.publish(vec![0xFA; payload_size].into()).unwrap(); sleep(Duration::from_millis(10)).await; diff --git a/livekit-datatrack/src/local/packetizer.rs b/livekit-datatrack/src/local/packetizer.rs index 4b9138fbe..dc096dcab 100644 --- a/livekit-datatrack/src/local/packetizer.rs +++ b/livekit-datatrack/src/local/packetizer.rs @@ -102,7 +102,8 @@ impl Packetizer { #[cfg(test)] mod tests { use super::*; - use crate::dtp::{E2eeExt, UserTimestampExt}; + use crate::dtp::{E2eeExt, UserTimestampExt, Handle}; + use fake::{Fake, Faker}; use test_case::test_matrix; #[test] @@ -115,9 +116,9 @@ mod tests { #[test_matrix([0, 128, 784], [256, 1024], [true, false])] fn test_packetize(payload_size: usize, mtu_size: usize, with_exts: bool) { - let handle = 1u32.try_into().unwrap(); - let e2ee = E2eeExt { key_index: 255, iv: [0xCD; 12] }; - let user_timestamp = UserTimestampExt(u64::MAX); + let handle: Handle = Faker.fake(); + let e2ee: E2eeExt = Faker.fake(); + let user_timestamp: UserTimestampExt = Faker.fake(); let mut packetizer = Packetizer::new(handle, mtu_size); diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 2ca5ec9d9..5c63a70a4 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -375,6 +375,7 @@ mod tests { use fake::{ faker::{internet::en::SafeEmail, lorem::en::Word}, Fake, + Faker }; use futures_util::StreamExt; use std::{collections::HashMap, time::Duration}; @@ -395,6 +396,8 @@ mod tests { async fn test_subscribe() { let publisher_identity: String = SafeEmail().fake(); let track_name: String = Word().fake(); + let track_sid: DataTrackSid = Faker.fake(); + let sub_handle: Handle = Faker.fake(); let options = ManagerOptions { decryption: None }; let (manager, manager_task, mut output_events) = Manager::new(options); @@ -405,8 +408,8 @@ mod tests { updates: HashMap::from([( publisher_identity.clone(), vec![DataTrackInfo { - sid: "DTR_1234".to_string().try_into().unwrap(), - handle: 1024u32.try_into().unwrap(), + sid: track_sid.clone(), + handle: Faker.fake(), // Pub handle name: track_name.clone(), uses_e2ee: false, }], @@ -427,6 +430,7 @@ mod tests { let track = wait_for_track.await; assert!(track.is_published()); assert_eq!(track.info().name, track_name); + assert_eq!(*track.info().sid(), track_sid); assert_eq!(track.publisher_identity(), publisher_identity); let simulate_subscriber_handles = async { @@ -434,14 +438,14 @@ mod tests { match event { OutputEvent::SubscriptionUpdated(event) => { assert!(event.subscribe); - assert_eq!(event.sid, "DTR_1234".to_string().try_into().unwrap()); + assert_eq!(event.sid, track_sid); time::sleep(Duration::from_millis(20)).await; // Simulate SFU reply let event = SubscriberHandlesEvent { mapping: HashMap::from([( - 64u32.try_into().unwrap(), - "DTR_1234".to_string().try_into().unwrap(), + sub_handle, + track_sid.clone() )]), }; _ = manager.send(event.into()); @@ -451,7 +455,7 @@ mod tests { } }; - time::timeout(Duration::from_secs(10), async { + time::timeout(Duration::from_secs(1), async { tokio::select! { _ = simulate_subscriber_handles => {} _ = track.subscribe() => {} diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index 3bd82077f..a19300873 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -105,3 +105,18 @@ impl Display for DataTrackSid { write!(f, "{}", self.0) } } + +#[cfg(test)] +impl fake::Dummy for DataTrackSid { + fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { + const BASE_57_ALPHABET: &[u8; 57] = + b"23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + let random_id: String = (0..12) + .map(|_| { + let idx = rng.random_range(0..BASE_57_ALPHABET.len()); + BASE_57_ALPHABET[idx] as char + }) + .collect(); + Self::try_from(format!("{}{}", Self::PREFIX, random_id)).unwrap() + } +} From bea6d0f9ecea538a2b1c190d66c74b2a31d388cb Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:49:16 +1100 Subject: [PATCH 139/232] Format --- livekit/src/prelude.rs | 4 ++-- livekit/src/proto.rs | 2 +- livekit/src/room/data_track.rs | 2 +- livekit/src/room/e2ee/manager.rs | 2 +- livekit/src/room/mod.rs | 8 ++++---- livekit/src/room/participant/local_participant.rs | 11 +++++++++-- livekit/src/room/participant/mod.rs | 2 +- livekit/src/rtc_engine/rtc_events.rs | 8 ++++++-- livekit/src/rtc_engine/rtc_session.rs | 7 +++++-- livekit/tests/data_track_test.rs | 8 ++++---- 10 files changed, 34 insertions(+), 20 deletions(-) diff --git a/livekit/src/prelude.rs b/livekit/src/prelude.rs index 4c8af91cd..569d9d122 100644 --- a/livekit/src/prelude.rs +++ b/livekit/src/prelude.rs @@ -16,8 +16,8 @@ pub use livekit_protocol::AudioTrackFeature; pub use crate::{ data_track::{ - DataTrackFrame, DataTrackInfo, DataTrackOptions, LocalDataTrack, - PublishError, PublishFrameError, PublishFrameErrorReason, RemoteDataTrack, + DataTrackFrame, DataTrackInfo, DataTrackOptions, LocalDataTrack, PublishError, + PublishFrameError, PublishFrameErrorReason, RemoteDataTrack, }, id::*, participant::{ diff --git a/livekit/src/proto.rs b/livekit/src/proto.rs index 3d791be8d..c70a90ec3 100644 --- a/livekit/src/proto.rs +++ b/livekit/src/proto.rs @@ -154,7 +154,7 @@ impl From for participant::ParticipantKind { participant_info::Kind::Egress => participant::ParticipantKind::Egress, participant_info::Kind::Sip => participant::ParticipantKind::Sip, participant_info::Kind::Agent => participant::ParticipantKind::Agent, - participant_info::Kind::Connector => participant::ParticipantKind::Connector + participant_info::Kind::Connector => participant::ParticipantKind::Connector, } } } diff --git a/livekit/src/room/data_track.rs b/livekit/src/room/data_track.rs index 28affb196..52e83917b 100644 --- a/livekit/src/room/data_track.rs +++ b/livekit/src/room/data_track.rs @@ -13,4 +13,4 @@ // limitations under the License. // Re-export everything in the "api" module publicly. -pub use livekit_datatrack::api::*; \ No newline at end of file +pub use livekit_datatrack::api::*; diff --git a/livekit/src/room/e2ee/manager.rs b/livekit/src/room/e2ee/manager.rs index dc18e699b..9539293d5 100644 --- a/livekit/src/room/e2ee/manager.rs +++ b/livekit/src/room/e2ee/manager.rs @@ -291,4 +291,4 @@ impl Debug for E2eeManager { .field("enabled", &self.inner.lock().enabled) .finish_non_exhaustive() } -} \ No newline at end of file +} diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index a053d8b0c..59ee68d17 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -23,9 +23,9 @@ use libwebrtc::{ RtcError, }; use livekit_api::signal_client::{SignalOptions, SignalSdkOptions}; +use livekit_datatrack::api::RemoteDataTrack; use livekit_protocol::observer::Dispatcher; use livekit_protocol::{self as proto, encryption}; -use livekit_datatrack::api::RemoteDataTrack; use livekit_runtime::JoinHandle; use parking_lot::RwLock; pub use proto::DisconnectReason; @@ -55,8 +55,8 @@ use crate::{ }, }; -pub mod data_track; pub mod data_stream; +pub mod data_track; pub mod e2ee; pub mod id; pub mod options; @@ -82,7 +82,7 @@ pub enum RoomError { #[error("already closed")] AlreadyClosed, #[error("request error: {reason:?} - {message}")] - Request { reason: proto::request_response::Reason, message: String } + Request { reason: proto::request_response::Reason, message: String }, } #[derive(Clone, Debug)] @@ -237,7 +237,7 @@ pub enum RoomEvent { participants: Vec, }, /// A remote participant published a data track. - RemoteDataTrackPublished(RemoteDataTrack) + RemoteDataTrackPublished(RemoteDataTrack), } #[derive(Debug, Clone, Copy, Eq, PartialEq)] diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 21b66cc93..016ce7bfd 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -24,10 +24,17 @@ use std::{ use super::{ConnectionQuality, ParticipantInner, ParticipantKind, ParticipantTrackPermission}; use crate::{ - ChatMessage, DataPacket, RoomSession, RpcAck, RpcRequest, RpcResponse, SipDTMF, Transcription, data_stream::{ + data_stream::{ ByteStreamInfo, ByteStreamWriter, StreamByteOptions, StreamResult, StreamTextOptions, TextStreamInfo, TextStreamWriter, - }, data_track::{self, DataTrack, DataTrackOptions, Local}, e2ee::EncryptionType, options::{self, TrackPublishOptions, compute_video_encodings, video_layers_from_encodings}, prelude::*, room::participant::rpc::{MAX_PAYLOAD_BYTES, RpcError, RpcErrorCode, RpcInvocationData}, rtc_engine::{EngineError, RtcEngine} + }, + data_track::{self, DataTrack, DataTrackOptions, Local}, + e2ee::EncryptionType, + options::{self, compute_video_encodings, video_layers_from_encodings, TrackPublishOptions}, + prelude::*, + room::participant::rpc::{RpcError, RpcErrorCode, RpcInvocationData, MAX_PAYLOAD_BYTES}, + rtc_engine::{EngineError, RtcEngine}, + ChatMessage, DataPacket, RoomSession, RpcAck, RpcRequest, RpcResponse, SipDTMF, Transcription, }; use chrono::Utc; use libwebrtc::{native::create_random_uuid, rtp_parameters::RtpEncodingParameters}; diff --git a/livekit/src/room/participant/mod.rs b/livekit/src/room/participant/mod.rs index a1a019bea..7c77975eb 100644 --- a/livekit/src/room/participant/mod.rs +++ b/livekit/src/room/participant/mod.rs @@ -44,7 +44,7 @@ pub enum ParticipantKind { Egress, Sip, Agent, - Connector + Connector, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] diff --git a/livekit/src/rtc_engine/rtc_events.rs b/livekit/src/rtc_engine/rtc_events.rs index af6bc0f46..a59aab58f 100644 --- a/livekit/src/rtc_engine/rtc_events.rs +++ b/livekit/src/rtc_engine/rtc_events.rs @@ -98,8 +98,12 @@ fn on_data_channel( ) -> rtc::peer_connection::OnDataChannel { Box::new(move |data_channel| { match data_channel.label().as_str() { - RELIABLE_DC_LABEL => data_channel.on_message(Some(on_message(emitter.clone(), DataPacketKind::Reliable))), - LOSSY_DC_LABEL => data_channel.on_message(Some(on_message(emitter.clone(), DataPacketKind::Lossy))), + RELIABLE_DC_LABEL => { + data_channel.on_message(Some(on_message(emitter.clone(), DataPacketKind::Reliable))) + } + LOSSY_DC_LABEL => { + data_channel.on_message(Some(on_message(emitter.clone(), DataPacketKind::Lossy))) + } _ => {} } let _ = emitter.send(RtcEvent::DataChannel { data_channel, target }); diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index b77108ed0..54f597e96 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -1099,7 +1099,10 @@ impl SessionInner { } proto::signal_response::Message::Update(mut update) => { let local_participant_identity = self.participant_info.identity.as_str().into(); - if let Ok(event) = dt::remote::event_from_participant_update(&mut update, local_participant_identity) { + if let Ok(event) = dt::remote::event_from_participant_update( + &mut update, + local_participant_identity, + ) { _ = self.remote_dt_manager.send(event.into()); } let _ = self @@ -1207,7 +1210,7 @@ impl SessionInner { DATA_TRACK_DC_LABEL => { handle_remote_dt_packets(&data_channel, self.remote_dt_manager.clone()); &self.sub_dt_transport - }, + } _ => return Ok(()), }; dc_ref.lock().replace(data_channel); diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 1af43bffe..986c7cd72 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -14,7 +14,7 @@ #[cfg(feature = "__lk-e2e-test")] use { - anyhow::{Ok, Result, anyhow}, + anyhow::{anyhow, Ok, Result}, common::test_rooms_with_options, futures_util::StreamExt, livekit::{data_track::DataTrackOptions, RoomEvent, RoomOptions}, @@ -32,7 +32,6 @@ mod common; #[test_case(120., 8_192)] #[test_log::test(tokio::test)] async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { - // How long to publish frames for. const PUBLISH_DURATION: Duration = Duration::from_secs(5); @@ -80,10 +79,11 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { let RoomEvent::RemoteDataTrackPublished(track) = event else { continue; }; - return Ok(track) + return Ok(track); } Err(anyhow!("No track published")) - }.await?; + } + .await?; log::info!("Got remote track: {}", track.info().sid()); assert!(track.is_published()); From d90b2894a7a44002e49bc1034df79198e40b431f Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:53:57 +1100 Subject: [PATCH 140/232] Rename for explicitness --- livekit-datatrack/src/local/manager.rs | 10 +++++----- livekit-datatrack/src/local/pipeline.rs | 2 +- livekit-datatrack/src/local/proto.rs | 6 +++--- livekit-datatrack/src/remote/manager.rs | 2 +- livekit-datatrack/src/remote/proto.rs | 2 +- livekit-datatrack/src/track.rs | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index d4425401d..802e4981c 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -282,7 +282,7 @@ impl ManagerTask { let task = LocalTrackTask { // TODO: handle cancellation - packetizer: Packetizer::new(info.handle, Self::TRANSPORT_MTU), + packetizer: Packetizer::new(info.pub_handle, Self::TRANSPORT_MTU), encryption: self.encryption.clone(), info: info.clone(), frame_rx, @@ -290,7 +290,7 @@ impl ManagerTask { event_out_tx: self.event_out_tx.clone(), }; let join_handle = livekit_runtime::spawn(task.run()); - self.descriptors.insert(info.handle, Descriptor::Active { state_tx: state_tx.clone(), join_handle }); + self.descriptors.insert(info.pub_handle, Descriptor::Active { state_tx: state_tx.clone(), join_handle }); let inner = LocalTrackInner { frame_tx, state_tx }; LocalDataTrack::new(info, inner) @@ -362,7 +362,7 @@ mod tests { let track_name: String = Word().fake(); let track_sid: DataTrackSid = Faker.fake(); - let handle: Handle = Faker.fake(); + let pub_handle: Handle = Faker.fake(); let options = ManagerOptions { encryption: None }; let (manager, manager_task, mut output_events) = Manager::new(options); @@ -380,7 +380,7 @@ mod tests { // SFU accepts publication let info = DataTrackInfo { sid: track_sid.clone(), - handle, + pub_handle, name: event.name, uses_e2ee: event.uses_e2ee, }; @@ -394,7 +394,7 @@ mod tests { packets_sent += 1; } OutputEvent::UnpublishRequest(event) => { - assert_eq!(event.handle, handle); + assert_eq!(event.handle, pub_handle); assert_eq!(packets_sent, packet_count); break; } diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 7410643bd..b15bd60c2 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -49,7 +49,7 @@ impl LocalTrackTask { } } if let LocalTrackState::Unpublished { initiator: UnpublishInitiator::Client } = state { - let event = UnpublishRequestEvent { handle: self.info.handle }; + let event = UnpublishRequestEvent { handle: self.info.pub_handle }; _ = self.event_out_tx.try_send(event.into()); } log::debug!("Task ended: sid={}", self.info.sid); diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index feece3f6a..9cfe641f9 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -45,7 +45,7 @@ impl TryFrom for PublishResultEvent { fn try_from(msg: proto::PublishDataTrackResponse) -> Result { let info: DataTrackInfo = msg.info.context("Missing info")?.try_into()?; - Ok(Self { handle: info.handle, result: Ok(info) }) + Ok(Self { handle: info.pub_handle, result: Ok(info) }) } } @@ -70,7 +70,7 @@ impl TryFrom for DataTrackInfo { other => Err(anyhow!("Unsupported E2EE type: {:?}", other))?, }; let sid: DataTrackSid = msg.sid.try_into().map_err(anyhow::Error::from)?; - Ok(Self { handle, sid, name: msg.name, uses_e2ee }) + Ok(Self { pub_handle: handle, sid, name: msg.name, uses_e2ee }) } } @@ -140,7 +140,7 @@ mod tests { assert_eq!(event.handle, 1u32.try_into().unwrap()); let info = event.result.expect("Expected ok result"); - assert_eq!(info.handle, 1u32.try_into().unwrap()); + assert_eq!(info.pub_handle, 1u32.try_into().unwrap()); assert_eq!(info.sid, "DTR_1234".to_string().try_into().unwrap()); assert_eq!(info.name, "track"); assert!(info.uses_e2ee); diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 5c63a70a4..c011ca497 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -409,7 +409,7 @@ mod tests { publisher_identity.clone(), vec![DataTrackInfo { sid: track_sid.clone(), - handle: Faker.fake(), // Pub handle + pub_handle: Faker.fake(), // Pub handle name: track_name.clone(), uses_e2ee: false, }], diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index 7e529f55a..022d1f6d6 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -155,7 +155,7 @@ mod tests { assert_eq!(track_info.len(), 1); let first = track_info.first().unwrap(); - assert_eq!(first.handle, 1u32.try_into().unwrap()); + assert_eq!(first.pub_handle, 1u32.try_into().unwrap()); assert_eq!(first.name, "track1"); assert_eq!(first.sid, "DTR_1234".to_string().try_into().unwrap()); } diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index a19300873..b301bdb98 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -21,7 +21,7 @@ use thiserror::Error; #[derive(Debug, Clone)] pub struct DataTrackInfo { pub(crate) sid: DataTrackSid, - pub(crate) handle: Handle, // TODO: consider removing (protocol level detail) + pub(crate) pub_handle: Handle, pub(crate) name: String, pub(crate) uses_e2ee: bool, } From aa8c555af606171de78b55b7bf50d1e60d3e1e4c Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:58:20 +1100 Subject: [PATCH 141/232] Doc --- 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 a18f1d3f8..2a159e11f 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -36,7 +36,7 @@ mod utils; /// Internal error. mod error; -/// Public APIs to be re-exported by the LiveKit crate. +/// Public APIs re-exported by the LiveKit crate. pub mod api { pub use crate::{error::*, frame::*, local::*, remote::*, track::*}; } From 6e5bed45105121a7eff9354931df24cc698ebba2 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:34:15 +1100 Subject: [PATCH 142/232] Depacketize multi-packet frames --- livekit-datatrack/src/remote/depacketizer.rs | 186 ++++++++++++++++++- 1 file changed, 179 insertions(+), 7 deletions(-) diff --git a/livekit-datatrack/src/remote/depacketizer.rs b/livekit-datatrack/src/remote/depacketizer.rs index b424977e4..079e33985 100644 --- a/livekit-datatrack/src/remote/depacketizer.rs +++ b/livekit-datatrack/src/remote/depacketizer.rs @@ -13,10 +13,15 @@ // limitations under the License. use crate::dtp::{Dtp, Extensions, FrameMarker}; -use bytes::Bytes; +use bytes::{Bytes, BytesMut}; +use std::collections::BTreeMap; -/// Converts packets into application-level frames. -pub struct Depacketizer; +/// Assembles packets into frames. +#[derive(Debug)] +pub struct Depacketizer { + /// Partial frame currently being assembled. + partial: Option, +} /// Output of [`Depacketizer`]. pub struct DepacketizerFrame { @@ -25,18 +30,185 @@ pub struct DepacketizerFrame { } impl Depacketizer { + /// Maximum number of packets to buffer per frame before dropping. + const MAX_BUFFER_PACKETS: usize = 128; + /// Creates a new depacketizer. pub fn new() -> Self { - Self + Self { partial: None } } /// Push a packet into the depacketizer, returning a complete frame if one is available. pub fn push(&mut self, dtp: Dtp) -> Option { match dtp.header.marker { - FrameMarker::Single => { - DepacketizerFrame { payload: dtp.payload, extensions: dtp.header.extensions }.into() + FrameMarker::Single => self.frame_from_single(dtp).into(), + FrameMarker::Start => { + self.begin_partial(dtp); + None + } + FrameMarker::Inter => { + self.push_to_partial(dtp); + None + } + FrameMarker::Final => { + self.push_to_partial(dtp); + self.finalize_partial() + } + } + } + + fn frame_from_single(&mut self, dtp: Dtp) -> DepacketizerFrame { + debug_assert!(dtp.header.marker == FrameMarker::Single); + + if self.partial.is_some() { + println!("Drop: interrupted"); + self.partial = None; + } + DepacketizerFrame { payload: dtp.payload, extensions: dtp.header.extensions } + } + + /// Begin assembling a new packet. + fn begin_partial(&mut self, dtp: Dtp) { + debug_assert!(dtp.header.marker == FrameMarker::Start); + + if self.partial.is_some() { + println!("Drop: interrupted"); + self.partial = None; + } + let start_sequence = dtp.header.sequence; + let payload_len = dtp.payload.len(); + + let partial = PartialFrame { + frame_number: dtp.header.frame_number, + start_sequence, + end_sequence: None, + extensions: dtp.header.extensions, + payloads: BTreeMap::from([(start_sequence, dtp.payload)]), + payload_len, + }; + self.partial = partial.into(); + } + + /// Push to the existing partial frame. + fn push_to_partial(&mut self, dtp: Dtp) { + debug_assert!(matches!(dtp.header.marker, FrameMarker::Inter | FrameMarker::Final)); + + let Some(mut partial) = self.partial.take() else { + println!("Drop: unknown frame"); + return; + }; + if dtp.header.frame_number != partial.frame_number { + println!("Drop: interrupted"); + return; + } + if partial.payloads.len() == Self::MAX_BUFFER_PACKETS { + println!("Drop: buffer full"); + return; + } + + partial.payload_len += dtp.payload.len(); + partial.payloads.insert(dtp.header.sequence, dtp.payload); + + if dtp.header.marker == FrameMarker::Final { + partial.end_sequence = dtp.header.sequence.into(); + } + + self.partial = Some(partial); + } + + /// If there is a partial frame and it has an end sequence set, + /// return a complete frame from it. + /// + fn finalize_partial(&mut self) -> Option { + let Some(mut partial) = self.partial.take() else { + log::trace!("Drop: unknown frame"); + return None; + }; + let Some(end_sequence) = partial.end_sequence else { + log::trace!("Drop: no end sequence"); + return None; + }; + + let mut sequence = partial.start_sequence; + let mut payload = BytesMut::with_capacity(partial.payload_len); + + while let Some(partial_payload) = partial.payloads.remove(&sequence) { + debug_assert!(payload.len() + partial_payload.len() <= payload.capacity()); + payload.extend(partial_payload); + + if sequence < end_sequence { + sequence = sequence.wrapping_add(1); + continue; } - _ => unimplemented!("Multi-packet frames not supported yet"), + return DepacketizerFrame { payload: payload.freeze(), extensions: partial.extensions } + .into(); + } + None + } +} + +#[derive(Debug)] +struct PartialFrame { + /// Frame number from the start packet. + frame_number: u16, + /// Sequence of the start packet. + start_sequence: u16, + /// End sequence, if final packet has been received. + end_sequence: Option, + /// Extensions from the start packet. + extensions: Extensions, + /// Mapping between sequence number and packet payload. + payloads: BTreeMap, + /// Sum of payload lengths. + payload_len: usize, +} + +#[cfg(test)] +mod tests { + use super::*; + use fake::{Fake, Faker}; + + #[test] + fn test_single_packet() { + let mut depacketizer = Depacketizer::new(); + + let mut dtp: Dtp = Faker.fake(); + dtp.header.marker = FrameMarker::Single; + + let frame = depacketizer.push(dtp.clone()).unwrap(); + assert_eq!(frame.payload, dtp.payload); + assert_eq!(frame.extensions, dtp.header.extensions); + } + + #[test] + fn test_multi_packet() { + const INTER_FRAMES: usize = 8; + let mut depacketizer = Depacketizer::new(); + + let mut dtp: Dtp = Faker.fake(); + dtp.header.marker = FrameMarker::Start; + + assert!(depacketizer.push(dtp.clone()).is_none()); + + for _ in 0..INTER_FRAMES { + dtp.header.marker = FrameMarker::Inter; + dtp.header.sequence += 1; + assert!(depacketizer.push(dtp.clone()).is_none()); + } + + dtp.header.marker = FrameMarker::Final; + dtp.header.sequence += 1; + + let frame = depacketizer.push(dtp.clone()).unwrap(); + assert_eq!(frame.extensions, dtp.header.extensions); + assert_eq!(frame.payload.len(), dtp.payload.len() * (INTER_FRAMES + 2)); // 1 start, 8 inter, 1 end + } + + impl fake::Dummy for Dtp { + fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { + let payload_len = rng.random_range(0..=1500); + let payload = (0..payload_len).map(|_| rng.random()).collect::(); + Self { header: Faker.fake_with_rng(rng), payload } } } } From 88231a04fda7e0d2732ec8e6e9255089f7de2e9d Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:13:09 +1100 Subject: [PATCH 143/232] Improve tests --- livekit-datatrack/src/local/packetizer.rs | 38 +++++++++----------- livekit-datatrack/src/remote/depacketizer.rs | 12 ++++--- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/livekit-datatrack/src/local/packetizer.rs b/livekit-datatrack/src/local/packetizer.rs index dc096dcab..873e1f701 100644 --- a/livekit-datatrack/src/local/packetizer.rs +++ b/livekit-datatrack/src/local/packetizer.rs @@ -102,32 +102,31 @@ impl Packetizer { #[cfg(test)] mod tests { use super::*; - use crate::dtp::{E2eeExt, UserTimestampExt, Handle}; + use crate::dtp::Handle; use fake::{Fake, Faker}; - use test_case::test_matrix; + use test_case::test_case; - #[test] - fn test_frame_marker() { - assert_eq!(Packetizer::frame_marker(0, 1), FrameMarker::Single); - assert_eq!(Packetizer::frame_marker(0, 10), FrameMarker::Start); - assert_eq!(Packetizer::frame_marker(4, 10), FrameMarker::Inter); - assert_eq!(Packetizer::frame_marker(9, 10), FrameMarker::Final); + #[test_case(0, 1, FrameMarker::Single)] + #[test_case(0, 10, FrameMarker::Start)] + #[test_case(4, 10, FrameMarker::Inter)] + #[test_case(9, 10, FrameMarker::Final)] + fn test_frame_marker(index: usize, packet_count: usize, expected_marker: FrameMarker) { + assert_eq!(Packetizer::frame_marker(index, packet_count), expected_marker); } - #[test_matrix([0, 128, 784], [256, 1024], [true, false])] - fn test_packetize(payload_size: usize, mtu_size: usize, with_exts: bool) { + #[test_case(0, 1_024 ; "zero_payload")] + #[test_case(128, 1_024 ; "single_packet")] + #[test_case(20_480, 1_024 ; "multi_packet")] + #[test_case(40_960, 16_000 ; "multi_packet_mtu_16000")] + fn test_packetize(payload_size: usize, mtu_size: usize) { let handle: Handle = Faker.fake(); - let e2ee: E2eeExt = Faker.fake(); - let user_timestamp: UserTimestampExt = Faker.fake(); + let extensions: Extensions = Faker.fake(); let mut packetizer = Packetizer::new(handle, mtu_size); let frame = PacketizerFrame { payload: Bytes::from(vec![0xAB; payload_size]), - extensions: Extensions { - e2ee: with_exts.then_some(e2ee), - user_timestamp: with_exts.then_some(user_timestamp), - }, + extensions: extensions.clone(), }; let packets = packetizer.packetize(frame).expect("Failed to packetize"); @@ -139,12 +138,9 @@ mod tests { for (index, packet) in packets.iter().enumerate() { assert_eq!(packet.header.marker, Packetizer::frame_marker(index, packets.len())); assert_eq!(packet.header.frame_number, 0); + assert_eq!(packet.header.track_handle, handle); assert_eq!(packet.header.sequence, index as u16); - assert_eq!(packet.header.extensions.e2ee, with_exts.then_some(e2ee)); - assert_eq!( - packet.header.extensions.user_timestamp, - with_exts.then_some(user_timestamp) - ); + assert_eq!(packet.header.extensions, extensions); } } } diff --git a/livekit-datatrack/src/remote/depacketizer.rs b/livekit-datatrack/src/remote/depacketizer.rs index 079e33985..ff967ca52 100644 --- a/livekit-datatrack/src/remote/depacketizer.rs +++ b/livekit-datatrack/src/remote/depacketizer.rs @@ -167,6 +167,7 @@ struct PartialFrame { mod tests { use super::*; use fake::{Fake, Faker}; + use test_case::test_case; #[test] fn test_single_packet() { @@ -180,9 +181,10 @@ mod tests { assert_eq!(frame.extensions, dtp.header.extensions); } - #[test] - fn test_multi_packet() { - const INTER_FRAMES: usize = 8; + #[test_case(0)] + #[test_case(8)] + #[test_case(Depacketizer::MAX_BUFFER_PACKETS - 2 ; "buffer_limit")] + fn test_multi_packet(inter_packets: usize) { let mut depacketizer = Depacketizer::new(); let mut dtp: Dtp = Faker.fake(); @@ -190,7 +192,7 @@ mod tests { assert!(depacketizer.push(dtp.clone()).is_none()); - for _ in 0..INTER_FRAMES { + for _ in 0..inter_packets { dtp.header.marker = FrameMarker::Inter; dtp.header.sequence += 1; assert!(depacketizer.push(dtp.clone()).is_none()); @@ -201,7 +203,7 @@ mod tests { let frame = depacketizer.push(dtp.clone()).unwrap(); assert_eq!(frame.extensions, dtp.header.extensions); - assert_eq!(frame.payload.len(), dtp.payload.len() * (INTER_FRAMES + 2)); // 1 start, 8 inter, 1 end + assert_eq!(frame.payload.len(), dtp.payload.len() * (inter_packets + 2)); } impl fake::Dummy for Dtp { From c517dfa9d65156c78f31bc507318bf225850f0d2 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:53:48 +1100 Subject: [PATCH 144/232] Batch outgoing packets per frame --- livekit-datatrack/src/local/manager.rs | 7 ++++--- livekit-datatrack/src/local/pipeline.rs | 10 ++++++---- livekit/src/rtc_engine/rtc_session.rs | 11 +++++++---- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 802e4981c..e84a77078 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -43,8 +43,8 @@ pub enum InputEvent { pub enum OutputEvent { PublishRequest(PublishRequestEvent), UnpublishRequest(UnpublishRequestEvent), - /// Encoded packet is ready to be sent over the transport. - PacketAvailable(Bytes), + /// Serialized packets are ready to be sent over the transport. + PacketsAvailable(Vec), } /// Result of a publish request. @@ -388,7 +388,8 @@ mod tests { PublishResultEvent { handle: event.handle, result: Ok(info) }; _ = manager.send(input_event.into()); } - OutputEvent::PacketAvailable(packet) => { + OutputEvent::PacketsAvailable(packets) => { + let packet = packets.into_iter().nth(0).unwrap(); let payload = Dtp::deserialize(packet).unwrap().payload; assert_eq!(payload.len(), payload_size); packets_sent += 1; diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index b15bd60c2..db6450b96 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -88,9 +88,11 @@ impl LocalTrackTask { return; } }; - for packet in packets { - let serialized = packet.serialize(); - _ = self.event_out_tx.try_send(serialized.into()); - } + + let packets: Vec<_> = packets.into_iter().map(|dtp| dtp.serialize()).collect(); + _ = self + .event_out_tx + .try_send(packets.into()) + .inspect_err(|err| log::debug!("Cannot send packet to transport: {}", err)); } } diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 54f597e96..5284784bd 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -849,10 +849,13 @@ impl SessionInner { .send(proto::signal_request::Message::UnpublishDataTrackRequest(event.into())) .await } - OutputEvent::PacketAvailable(packet) => { - _ = self.dt_transport.send(&packet, true).inspect_err(|e| { - log::error!("Failed to send data track packet over transport: {}", e) - }); + OutputEvent::PacketsAvailable(packets) => { + for packet in packets { + if let Err(err) = self.dt_transport.send(&packet, true) { + log::error!("Failed to send packet over transport: {}", err); + break; // Drop the rest of the batch + } + } } } } From 050bd9758439f6e2b36fabc7443c04e03652c020 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:20:01 +1100 Subject: [PATCH 145/232] Log level --- livekit-datatrack/src/remote/depacketizer.rs | 10 +++++----- livekit/src/rtc_engine/rtc_session.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/livekit-datatrack/src/remote/depacketizer.rs b/livekit-datatrack/src/remote/depacketizer.rs index ff967ca52..59fd02dd0 100644 --- a/livekit-datatrack/src/remote/depacketizer.rs +++ b/livekit-datatrack/src/remote/depacketizer.rs @@ -61,7 +61,7 @@ impl Depacketizer { debug_assert!(dtp.header.marker == FrameMarker::Single); if self.partial.is_some() { - println!("Drop: interrupted"); + log::trace!("Drop: interrupted"); self.partial = None; } DepacketizerFrame { payload: dtp.payload, extensions: dtp.header.extensions } @@ -72,7 +72,7 @@ impl Depacketizer { debug_assert!(dtp.header.marker == FrameMarker::Start); if self.partial.is_some() { - println!("Drop: interrupted"); + log::trace!("Drop: interrupted"); self.partial = None; } let start_sequence = dtp.header.sequence; @@ -94,15 +94,15 @@ impl Depacketizer { debug_assert!(matches!(dtp.header.marker, FrameMarker::Inter | FrameMarker::Final)); let Some(mut partial) = self.partial.take() else { - println!("Drop: unknown frame"); + log::trace!("Drop: unknown frame"); return; }; if dtp.header.frame_number != partial.frame_number { - println!("Drop: interrupted"); + log::trace!("Drop: interrupted"); return; } if partial.payloads.len() == Self::MAX_BUFFER_PACKETS { - println!("Drop: buffer full"); + log::trace!("Drop: buffer full"); return; } diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 5284784bd..f08b4e634 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -852,7 +852,7 @@ impl SessionInner { OutputEvent::PacketsAvailable(packets) => { for packet in packets { if let Err(err) = self.dt_transport.send(&packet, true) { - log::error!("Failed to send packet over transport: {}", err); + log::trace!("Failed to send packet over transport: {}", err); break; // Drop the rest of the batch } } From 89983e11046697c04008d84837734e2ca5d4f7e5 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:23:35 +1100 Subject: [PATCH 146/232] Add test cases --- livekit/tests/data_track_test.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 986c7cd72..e1980d07f 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -29,7 +29,8 @@ use { mod common; #[cfg(feature = "__lk-e2e-test")] -#[test_case(120., 8_192)] +#[test_case(120., 8_192 ; "high_fps_single_packet")] +#[test_case(10., 196_608 ; "low_fps_multi_packet")] #[test_log::test(tokio::test)] async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { // How long to publish frames for. From 5aab7224fce0cb30e27d8e7a45693c34815c06f5 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:23:48 +1100 Subject: [PATCH 147/232] Add missing dev dependency --- Cargo.lock | 1 + livekit/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 6049f3934..4e97fbb22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3094,6 +3094,7 @@ dependencies = [ "semver", "serde", "serde_json", + "test-case", "test-log", "thiserror 1.0.69", "tokio", diff --git a/livekit/Cargo.toml b/livekit/Cargo.toml index 6b7d9fa59..76c487c34 100644 --- a/livekit/Cargo.toml +++ b/livekit/Cargo.toml @@ -49,3 +49,4 @@ bmrng = "0.5.2" [dev-dependencies] anyhow = "1.0.99" test-log = "0.2.18" +test-case = "3.3" \ No newline at end of file From fd38b5337ca9bb356a912bb820e1bfea124c0280 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:37:52 +1100 Subject: [PATCH 148/232] Allow overriding video grants for e2e tests --- livekit/tests/common/e2e/mod.rs | 42 ++++++++++++++++++++---- livekit/tests/data_channel_encryption.rs | 2 +- livekit/tests/data_track_test.rs | 3 +- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/livekit/tests/common/e2e/mod.rs b/livekit/tests/common/e2e/mod.rs index 44c27010c..fc0834847 100644 --- a/livekit/tests/common/e2e/mod.rs +++ b/livekit/tests/common/e2e/mod.rs @@ -44,14 +44,43 @@ impl TestEnvironment { } } +#[derive(Clone)] +pub struct TestRoomOptions { + /// Grants for the generated token. + grants: VideoGrants, + /// Options used for creating the [`Room`]. + room: RoomOptions, +} + +impl Default for TestRoomOptions { + fn default() -> Self { + Self { + grants: VideoGrants { room_join: true, ..Default::default() }, + room: Default::default(), + } + } +} + +impl From for TestRoomOptions { + fn from(room: RoomOptions) -> Self { + Self { room, ..Default::default() } + } +} + +impl From for TestRoomOptions { + fn from(grants: VideoGrants) -> Self { + Self { grants, ..Default::default() } + } +} + /// Creates the specified number of connections to a shared room for testing. pub async fn test_rooms(count: usize) -> Result)>> { - test_rooms_with_options((0..count).map(|_| RoomOptions::default())).await + test_rooms_with_options((0..count).map(|_| TestRoomOptions::default())).await } /// Creates multiple connections to a shared room for testing, one for each configuration. pub async fn test_rooms_with_options( - options: impl IntoIterator, + options: impl IntoIterator, ) -> Result)>> { let test_env = TestEnvironment::from_env_or_defaults(); let room_name = format!("test_room_{}", create_random_uuid()); @@ -59,17 +88,16 @@ pub async fn test_rooms_with_options( let tokens = options .into_iter() .enumerate() - .map(|(id, options)| -> Result<(String, RoomOptions)> { - let grants = - VideoGrants { room_join: true, room: room_name.clone(), ..Default::default() }; + .map(|(id, mut options)| -> Result<(String, RoomOptions)> { + options.grants.room = room_name.clone(); let token = AccessToken::with_api_key(&test_env.api_key, &test_env.api_secret) .with_ttl(Duration::from_secs(30 * 60)) // 30 minutes - .with_grants(grants) + .with_grants(options.grants) .with_identity(&format!("p{}", id)) .with_name(&format!("Participant {}", id)) .to_jwt() .context("Failed to generate JWT")?; - Ok((token, options)) + Ok((token, options.room)) }) .collect::>>()?; diff --git a/livekit/tests/data_channel_encryption.rs b/livekit/tests/data_channel_encryption.rs index 2e1515423..f36207295 100644 --- a/livekit/tests/data_channel_encryption.rs +++ b/livekit/tests/data_channel_encryption.rs @@ -49,7 +49,7 @@ async fn test_data_channel_encryption() -> Result<()> { options2.encryption = Some(E2eeOptions { key_provider: key_provider2, encryption_type: EncryptionType::Gcm }); - let mut rooms = test_rooms_with_options([options1, options2]).await?; + let mut rooms = test_rooms_with_options([options1.into(), options2.into()]).await?; let (sending_room, _) = rooms.pop().unwrap(); let (receiving_room, mut receiving_event_rx) = rooms.pop().unwrap(); diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index e1980d07f..6afa757ad 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -15,7 +15,7 @@ #[cfg(feature = "__lk-e2e-test")] use { anyhow::{anyhow, Ok, Result}, - common::test_rooms_with_options, + common::{TestRoomOptions, test_rooms_with_options}, futures_util::StreamExt, livekit::{data_track::DataTrackOptions, RoomEvent, RoomOptions}, std::{iter, time::Duration}, @@ -43,6 +43,7 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { // Temporary workaround until auto subscribe is disabled on the SFU. let mut room_options = RoomOptions::default(); room_options.auto_subscribe = false; + let room_options: TestRoomOptions = room_options.into(); let mut rooms = test_rooms_with_options(iter::repeat(room_options.clone()).take(2)).await?; From 12463dcb35764f8d61bf7c44fb40e21bb5e35e7b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:54:03 +1100 Subject: [PATCH 149/232] Remove explicit setting of auto subscribe to false No longer affects data tracks --- examples/basic_data_track/src/subscriber.rs | 4 +--- livekit/tests/data_track_test.rs | 15 ++++----------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/examples/basic_data_track/src/subscriber.rs b/examples/basic_data_track/src/subscriber.rs index 69252051e..7d906273d 100644 --- a/examples/basic_data_track/src/subscriber.rs +++ b/examples/basic_data_track/src/subscriber.rs @@ -11,9 +11,7 @@ async fn main() -> Result<()> { let url = env::var("LIVEKIT_URL").expect("LIVEKIT_URL is not set"); let token = env::var("LIVEKIT_TOKEN").expect("LIVEKIT_TOKEN is not set"); - let mut options = RoomOptions::default(); - options.auto_subscribe = false; - let (_, rx) = Room::connect(&url, &token, options).await?; + let (_, rx) = Room::connect(&url, &token, RoomOptions::default()).await?; tokio::select! { _ = handle_first_publication(rx) => {} diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 6afa757ad..f826bf7d3 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -15,10 +15,10 @@ #[cfg(feature = "__lk-e2e-test")] use { anyhow::{anyhow, Ok, Result}, - common::{TestRoomOptions, test_rooms_with_options}, + common::test_rooms, futures_util::StreamExt, - livekit::{data_track::DataTrackOptions, RoomEvent, RoomOptions}, - std::{iter, time::Duration}, + livekit::{data_track::DataTrackOptions, RoomEvent}, + std::time::Duration, test_case::test_case, tokio::{ time::{self, timeout}, @@ -40,14 +40,7 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { // order for the test to pass. const MIN_PERCENTAGE: f32 = 0.95; - // Temporary workaround until auto subscribe is disabled on the SFU. - let mut room_options = RoomOptions::default(); - room_options.auto_subscribe = false; - let room_options: TestRoomOptions = room_options.into(); - - let mut rooms = test_rooms_with_options(iter::repeat(room_options.clone()).take(2)).await?; - - // let mut rooms = test_rooms(2).await?; + let mut rooms = test_rooms(2).await?; let (pub_room, _) = rooms.pop().unwrap(); let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); From 679eefb9cb89f31b6a3d09befe86c32aaa88f064 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 15 Jan 2026 17:55:35 +1100 Subject: [PATCH 150/232] Doc --- livekit-datatrack/src/frame.rs | 12 +++++- livekit-datatrack/src/local/mod.rs | 60 ++++++++++++++++++++++++++++-- livekit-datatrack/src/track.rs | 45 +++++++++++----------- 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/livekit-datatrack/src/frame.rs b/livekit-datatrack/src/frame.rs index c88cbc691..7feca350b 100644 --- a/livekit-datatrack/src/frame.rs +++ b/livekit-datatrack/src/frame.rs @@ -15,7 +15,7 @@ use bytes::Bytes; use core::fmt; -/// Application-level frame published to a data track. +/// Frame published over a data track containing a payload and optional metadata. #[derive(Clone, Default)] pub struct DataTrackFrame { pub(crate) payload: Bytes, @@ -35,7 +35,15 @@ impl DataTrackFrame { } impl DataTrackFrame { - /// Creates a data track frame with the given bytes. + /// Creates a data track frame with the given payload. + /// + /// # Examples + /// + /// ``` + /// # use livekit_datatrack::api::DataTrackFrame; + /// let frame = DataTrackFrame::new(vec![0xFA; 256]); + /// ``` + /// pub fn new(payload: impl Into) -> Self { Self { payload: payload.into(), ..Default::default() } } diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 8c5c0d762..a63b26b04 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -24,8 +24,8 @@ use tokio::sync::{mpsc, watch}; pub(crate) mod manager; pub(crate) mod proto; -mod pipeline; mod packetizer; +mod pipeline; /// Data track published by the local participant. pub type LocalDataTrack = DataTrack; @@ -48,7 +48,35 @@ impl DataTrack { } impl DataTrack { - /// Publish a frame onto the track. + /// Publishes a frame to the track. + /// + /// # Example + /// + /// ``` + /// # use livekit_datatrack::api::{LocalDataTrack, DataTrackFrame, PublishFrameError}; + /// # fn example(track: LocalDataTrack) -> Result<(), PublishFrameError> { + /// fn read_sensor() -> Vec { + /// // Read some sensor data... + /// vec![0xFA; 16] + /// } + /// + /// let frame = read_sensor().into(); // Convert to frame + /// track.publish(frame)?; + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// See [`DataTrackFrame`] for more ways to construct a frame and how to attach metadata. + /// + /// # Errors + /// + /// Publishing a frame can fail for several reasons: + /// + /// - The track has been unpublished by the local participant or SFU + /// - The room is no longer connected + /// - Frames are being published too fast + /// pub fn publish(&self, frame: DataTrackFrame) -> Result<(), PublishFrameError> { if !self.is_published() { return Err(PublishFrameError::new(frame, PublishFrameErrorReason::TrackUnpublished)); @@ -59,11 +87,14 @@ impl DataTrack { } /// Whether or not the track is still published. + /// + /// Once the track has been unpublished, calls to [`Self::publish`] will fail. + /// pub fn is_published(&self) -> bool { self.inner().state_tx.borrow().is_published() } - /// Unpublish the track. + /// Unpublishes the track. pub fn unpublish(self) { self.inner().local_unpublish(); } @@ -108,6 +139,14 @@ impl PublishFrameError { } /// Options for publishing a data track. +/// +/// Create options for publishing a track named "my_track" with end-to-end encryption disabled: +/// ``` +/// # use livekit_datatrack::api::DataTrackOptions; +/// let options = DataTrackOptions::with_name("my_track") +/// .disable_e2ee(true); +/// ``` +/// #[derive(Clone, Debug)] pub struct DataTrackOptions { pub(crate) name: String, @@ -115,9 +154,22 @@ pub struct DataTrackOptions { } impl DataTrackOptions { + /// Creates options with the given track name. + /// + /// The track name is used to identify the track to other participants. + /// + /// # Requirements + /// - Must not be empty + /// - Must be unique per publisher + /// pub fn with_name(name: impl Into) -> Self { Self { name: name.into(), disable_e2ee: false } } + + /// Disable end-to-end encryption. + /// + /// By default, room settings are used. + /// pub fn disable_e2ee(self, disabled: bool) -> Self { Self { disable_e2ee: disabled, ..self } } @@ -139,7 +191,7 @@ pub enum PublishError { Internal(#[from] InternalError), } -/// An error that can occur when publishing a frame onto a data track. +/// Frame could not be published to the track. #[derive(Debug, Error)] #[error("Failed to publish frame: {reason}")] pub struct PublishFrameError { diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index b301bdb98..91622c6df 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -17,6 +17,28 @@ use from_variants::FromVariants; use std::{fmt::Display, marker::PhantomData, ops::Deref, sync::Arc}; use thiserror::Error; +/// Track for communicating application-specific data between participants in room. +#[derive(Debug, Clone)] +pub struct DataTrack { + pub(crate) info: Arc, + pub(crate) inner: Arc, + /// Marker indicating local or remote. + pub(crate) _location: PhantomData, +} + +#[derive(Debug, Clone, FromVariants)] +pub(crate) enum DataTrackInner { + Local(crate::local::LocalTrackInner), + Remote(crate::remote::RemoteTrackInner), +} + +impl DataTrack { + /// Information about the data track. + pub fn info(&self) -> &DataTrackInfo { + &self.info + } +} + /// Information about a published data track. #[derive(Debug, Clone)] pub struct DataTrackInfo { @@ -31,7 +53,7 @@ impl DataTrackInfo { pub fn sid(&self) -> &DataTrackSid { &self.sid } - /// Name of the track assigned when published. + /// Name of the track assigned by the publisher. pub fn name(&self) -> &str { &self.name } @@ -41,27 +63,6 @@ impl DataTrackInfo { } } -#[derive(Debug, Clone)] -pub struct DataTrack { - pub(crate) info: Arc, - pub(crate) inner: Arc, - /// Marker indicating local or remote. - pub(crate) _location: PhantomData, -} - -#[derive(Debug, Clone, FromVariants)] -pub(crate) enum DataTrackInner { - Local(crate::local::LocalTrackInner), - Remote(crate::remote::RemoteTrackInner), -} - -impl DataTrack { - /// Information about the data track such as name. - pub fn info(&self) -> &DataTrackInfo { - &self.info - } -} - /// SFU-assigned identifier uniquely identifying a data track. #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct DataTrackSid(String); From 67da84a4d9549f9bf8730cab12dc3373ac8117a9 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:06:03 +1100 Subject: [PATCH 151/232] Clarify type intention (local) --- livekit-datatrack/src/local/manager.rs | 190 +++++++++++++------------ livekit/src/rtc_engine/rtc_session.rs | 20 +-- 2 files changed, 112 insertions(+), 98 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index e84a77078..f511d9afe 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -99,43 +99,42 @@ pub struct PublishTimeoutEvent { handle: Handle, } -#[derive(Debug, Clone, Copy)] -pub(crate) enum UnpublishInitiator { - Client, - Sfu, - Shutdown, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) enum LocalTrackState { - Published, - Unpublished { initiator: UnpublishInitiator }, -} - -impl LocalTrackState { - pub fn is_published(&self) -> bool { - matches!(self, Self::Published) - } -} - +/// Options for creating a [`Manager`]. #[derive(Debug)] pub struct ManagerOptions { + /// Provider to use for encrypting outgoing frame payloads. + /// + /// If none, end-to-end encryption will be disabled for all published tracks. + /// pub encryption: Option>, } -/// Manager for local data tracks. -#[derive(Debug, Clone)] +/// System for managing data track publications. pub struct Manager { - event_in_tx: mpsc::Sender, + encryption: Option>, + event_in_tx: mpsc::WeakSender, + event_in_rx: mpsc::Receiver, + event_out_tx: mpsc::Sender, + handle_allocator: dtp::HandleAllocator, + descriptors: HashMap, } impl Manager { - pub fn new(options: ManagerOptions) -> (Self, ManagerTask, impl Stream) { - let (event_in_tx, event_in_rx) = mpsc::channel(Self::INPUT_BUFFER_SIZE); - let (event_out_tx, signal_out_rx) = mpsc::channel(Self::OUTPUT_BUFFER_SIZE); - let manager = Self { event_in_tx: event_in_tx.clone() }; - let task = ManagerTask { + /// Creates a new manager task. + /// + /// Returns a tuple containing the following: + /// + /// - The manager task itself to be spawned by the caller (see [`Manager::run`]). + /// - Channel for sending [`InputEvent`]s to be processed by the manager task. + /// - Stream for receiving [`OutputEvent`]s produced by the manager task. + /// + pub fn new(options: ManagerOptions) -> (Self, ManagerInput, impl Stream) { + let (event_in_tx, event_in_rx) = mpsc::channel(4); // TODO: tune buffer size + let (event_out_tx, signal_out_rx) = mpsc::channel(4); + + let event_in = ManagerInput { event_in_tx: event_in_tx.clone() }; + let task = Manager { encryption: options.encryption, event_in_tx: event_in_tx.downgrade(), event_in_rx, @@ -145,63 +144,13 @@ impl Manager { }; let event_out_stream = ReceiverStream::new(signal_out_rx); - (manager, task, event_out_stream) - } - - /// Sends an input event to the manager's task to be processed. - pub fn send(&self, event: InputEvent) -> Result<(), InternalError> { - Ok(self.event_in_tx.try_send(event).context("Failed to handle input event")?) - } - - /// Publishes a data track with the given options. - pub async fn publish_track( - &self, - options: DataTrackOptions, - ) -> Result { - let (result_tx, result_rx) = oneshot::channel(); - let event = PublishEvent { options, result_tx }; - - self.event_in_tx.try_send(event.into()).map_err(|_| PublishError::Disconnected)?; - let track = result_rx.await.map_err(|_| PublishError::Disconnected)??; - - Ok(track) + (task, event_in, event_out_stream) } - /// Number of [`InputEvent`]s to buffer. - const INPUT_BUFFER_SIZE: usize = 4; - - /// Number of [`OutputEvent`]s to buffer. - const OUTPUT_BUFFER_SIZE: usize = 4; -} - -#[derive(Debug)] -enum Descriptor { - /// Publication is awaiting SFU response. + /// Run the manager task, consuming self. /// - /// The associated channel is used to send a result to the user, - /// either the local track or a publish error. - /// - Pending(oneshot::Sender>), - /// Publication is active. + /// The manager will continue running until receiving [`InputEvent::Shutdown`]. /// - /// The associated channel is used to send state updates to the track's task. - /// - Active { - state_tx: watch::Sender, - join_handle: livekit_runtime::JoinHandle<()> - }, -} - -pub struct ManagerTask { - encryption: Option>, - event_in_tx: mpsc::WeakSender, - event_in_rx: mpsc::Receiver, - event_out_tx: mpsc::Sender, - handle_allocator: dtp::HandleAllocator, - descriptors: HashMap, -} - -impl ManagerTask { pub async fn run(mut self) { log::debug!("Task started"); while let Some(event) = self.event_in_rx.recv().await { @@ -336,6 +285,71 @@ impl ManagerTask { const TRANSPORT_MTU: usize = 16_000; } +#[derive(Debug)] +enum Descriptor { + /// Publication is awaiting SFU response. + /// + /// The associated channel is used to send a result to the user, + /// either the local track or a publish error. + /// + Pending(oneshot::Sender>), + /// Publication is active. + /// + /// The associated channel is used to send state updates to the track's task. + /// + Active { + state_tx: watch::Sender, + join_handle: livekit_runtime::JoinHandle<()> + }, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum UnpublishInitiator { + Client, + Sfu, + Shutdown, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum LocalTrackState { + Published, + Unpublished { initiator: UnpublishInitiator }, +} + +impl LocalTrackState { + pub fn is_published(&self) -> bool { + matches!(self, Self::Published) + } +} + +/// Channel for sending [`InputEvent`]s to [`Manager`]. +#[derive(Debug, Clone)] +pub struct ManagerInput { + event_in_tx: mpsc::Sender, +} + +impl ManagerInput { + + /// Sends an input event to the manager's task to be processed. + pub fn send(&self, event: InputEvent) -> Result<(), InternalError> { + Ok(self.event_in_tx.try_send(event).context("Failed to handle input event")?) + } + + /// Publishes a data track with given options. + pub async fn publish_track( + &self, + options: DataTrackOptions, + ) -> Result { + let (result_tx, result_rx) = oneshot::channel(); + let event = PublishEvent { options, result_tx }; + + self.event_in_tx.try_send(event.into()).map_err(|_| PublishError::Disconnected)?; + let track = result_rx.await.map_err(|_| PublishError::Disconnected)??; + + Ok(track) + } +} + #[cfg(test)] mod tests { use super::*; @@ -347,10 +361,10 @@ mod tests { #[tokio::test] async fn test_task_shutdown() { let options = ManagerOptions { encryption: None }; - let (manager, manager_task, _) = Manager::new(options); + let (manager, input, _) = Manager::new(options); - let join_handle = livekit_runtime::spawn(manager_task.run()); - _ = manager.send(InputEvent::Shutdown); + let join_handle = livekit_runtime::spawn(manager.run()); + _ = input.send(InputEvent::Shutdown); time::timeout(Duration::from_secs(1), join_handle).await.unwrap(); } @@ -365,13 +379,13 @@ mod tests { let pub_handle: Handle = Faker.fake(); let options = ManagerOptions { encryption: None }; - let (manager, manager_task, mut output_events) = Manager::new(options); - livekit_runtime::spawn(manager_task.run()); + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); let track_name_clone = track_name.clone(); let handle_events = async { let mut packets_sent = 0; - while let Some(event) = output_events.next().await { + while let Some(event) = output.next().await { match event { OutputEvent::PublishRequest(event) => { assert!(!event.uses_e2ee); @@ -386,7 +400,7 @@ mod tests { }; let input_event = PublishResultEvent { handle: event.handle, result: Ok(info) }; - _ = manager.send(input_event.into()); + _ = input.send(input_event.into()); } OutputEvent::PacketsAvailable(packets) => { let packet = packets.into_iter().nth(0).unwrap(); @@ -404,7 +418,7 @@ mod tests { }; let publish_track = async { let track_options = DataTrackOptions::with_name(track_name.clone()); - let track = manager.publish_track(track_options).await.unwrap(); + let track = input.publish_track(track_options).await.unwrap(); assert!(!track.info().uses_e2ee()); assert_eq!(track.info().name(), track_name); assert_eq!(*track.info().sid(), track_sid); diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index f08b4e634..3e618da7d 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -379,7 +379,7 @@ struct SessionInner { e2ee_manager: Option, // Data track managers - local_dt_manager: dt::local::Manager, + local_dt_input: dt::local::ManagerInput, remote_dt_manager: dt::remote::Manager, } @@ -489,7 +489,7 @@ impl RtcSession { .filter(|m| m.enabled()) .map(|m| Arc::new(m) as Arc), }; - let (local_dt_manager, local_dt_task, local_dt_events) = + let (local_dt_manager, local_dt_input, local_dt_output) = dt::local::Manager::new(local_dt_options); let remote_dt_options = dt::remote::ManagerOptions { @@ -536,7 +536,7 @@ impl RtcSession { negotiation_queue: NegotiationQueue::new(), pending_requests: Default::default(), e2ee_manager, - local_dt_manager, + local_dt_input, remote_dt_manager, }); @@ -549,8 +549,8 @@ impl RtcSession { livekit_runtime::spawn(inner.clone().data_channel_task(dc_events, close_rx.clone())); let local_dt_forward_task = - livekit_runtime::spawn(inner.clone().local_dt_forward_task(local_dt_events)); - let local_dt_task = livekit_runtime::spawn(local_dt_task.run()); + livekit_runtime::spawn(inner.clone().local_dt_forward_task(local_dt_output)); + let local_dt_task = livekit_runtime::spawn(local_dt_manager.run()); let remote_dt_forward_task = livekit_runtime::spawn(inner.clone().remote_dt_forward_task(remote_dt_events)); @@ -611,7 +611,7 @@ impl RtcSession { let _ = handle.signal_task.await; let _ = handle.dc_task.await; - _ = self.inner.local_dt_manager.send(dt::local::InputEvent::Shutdown); + _ = self.inner.local_dt_input.send(dt::local::InputEvent::Shutdown); let _ = handle.local_dt_task.await; let _ = handle.local_dt_forward_task.await; @@ -1142,7 +1142,7 @@ impl SessionInner { if let Some(event) = dt::local::publish_result_from_request_response(&request_response) { - _ = self.local_dt_manager.send(event.into()); + _ = self.local_dt_input.send(event.into()); return Ok(()); } let mut pending_requests = self.pending_requests.lock(); @@ -1152,11 +1152,11 @@ impl SessionInner { } proto::signal_response::Message::PublishDataTrackResponse(publish_res) => { let event: dt::local::PublishResultEvent = publish_res.try_into()?; - _ = self.local_dt_manager.send(event.into()); + _ = self.local_dt_input.send(event.into()); } proto::signal_response::Message::UnpublishDataTrackResponse(unpublish_res) => { let event: dt::local::UnpublishEvent = unpublish_res.try_into()?; - _ = self.local_dt_manager.send(event.into()); + _ = self.local_dt_input.send(event.into()); } proto::signal_response::Message::DataTrackSubscriberHandles(subscriber_handles) => { let event: dt::remote::SubscriberHandlesEvent = subscriber_handles.try_into()?; @@ -1652,7 +1652,7 @@ impl SessionInner { .await .inspect_err(|err| log::debug!("Data track transport not connected: {}", err)) .map_err(|_| PublishError::Disconnected)?; // TODO: better error mapping - self.local_dt_manager.publish_track(options).await + self.local_dt_input.publish_track(options).await } async fn publish_data( From aabe13ff1f8c792d7ded07bae0ffbecff5bdd17e Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:31:06 +1100 Subject: [PATCH 152/232] Clarify type intention (remote) --- livekit-datatrack/src/remote/manager.rs | 168 +++++++++++++----------- livekit/src/rtc_engine/rtc_session.rs | 22 ++-- 2 files changed, 101 insertions(+), 89 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index c011ca497..e285ee769 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -91,37 +91,46 @@ pub struct SubscriptionUpdatedEvent { pub subscribe: bool, } -#[derive(Debug, Clone, Copy)] -pub(crate) enum TrackState { - Published, - Unpublished, -} - -impl TrackState { - pub fn is_published(&self) -> bool { - !matches!(self, Self::Unpublished) - } -} - +/// Options for creating a [`Manager`]. #[derive(Debug)] pub struct ManagerOptions { + /// Provider to use for decrypting incoming frame payloads. + /// + /// If none, remote tracks using end-to-end encryption will not be available + /// for subscription. + /// pub decryption: Option>, } -/// Manager for remote data tracks. -#[derive(Debug, Clone)] +/// System for managing data track subscriptions. pub struct Manager { - event_in_tx: mpsc::Sender, + decryption: Option>, + event_in_tx: mpsc::WeakSender, + event_in_rx: mpsc::Receiver, + event_out_tx: mpsc::Sender, + + /// Mapping between track SID and descriptor. + descriptors: HashMap, + /// Bidirectional mapping between track SID and subscriber handle. + sub_handles: HandleMap, } impl Manager { - /// Creates a new manager with the specified options. - pub fn new(options: ManagerOptions) -> (Self, ManagerTask, impl Stream) { - let (event_in_tx, event_in_rx) = mpsc::channel(Self::INPUT_BUFFER_SIZE); - let (event_out_tx, event_out_rx) = mpsc::channel(Self::OUTPUT_BUFFER_SIZE); - let manager = Manager { event_in_tx: event_in_tx.clone() }; - let task = ManagerTask { + /// Creates a new manager. + /// + /// Returns a tuple containing the following: + /// + /// - The manager itself to be spawned by the caller (see [`Manager::run`]). + /// - Channel for sending [`InputEvent`]s to be processed by the manager. + /// - Stream for receiving [`OutputEvent`]s produced by the manager. + /// + pub fn new(options: ManagerOptions) -> (Self, ManagerInput, impl Stream) { + let (event_in_tx, event_in_rx) = mpsc::channel(4); // TODO: tune buffer size + let (event_out_tx, event_out_rx) = mpsc::channel(4); + + let event_in = ManagerInput { event_in_tx: event_in_tx.clone() }; + let manager = Manager { decryption: options.decryption, event_in_tx: event_in_tx.downgrade(), event_in_rx, @@ -130,56 +139,14 @@ impl Manager { sub_handles: HandleMap::default(), }; - let event_out_stream = ReceiverStream::new(event_out_rx); - (manager, task, event_out_stream) - } - - /// Sends an input event to the manager's task to be processed. - pub fn send(&self, event: InputEvent) -> Result<(), InternalError> { - // TODO: try_send for data - Ok(self.event_in_tx.try_send(event).context("Failed to send input event")?) + let event_out = ReceiverStream::new(event_out_rx); + (manager, event_in, event_out) } - /// Number of [`InputEvent`]s to buffer. - const INPUT_BUFFER_SIZE: usize = 4; - - /// Number of [`OutputEvent`]s to buffer. - const OUTPUT_BUFFER_SIZE: usize = 4; -} - -#[derive(Debug)] -struct Descriptor { - info: Arc, - state_tx: watch::Sender, - state: DescriptorState, -} - -#[derive(Debug)] -enum DescriptorState { - Available, - PendingSubscriberHandle { - result_txs: Vec>, - }, - Subscribed { - packet_tx: mpsc::Sender, - frame_tx: broadcast::Sender, - join_handle: livekit_runtime::JoinHandle<()>, - }, -} - -pub struct ManagerTask { - decryption: Option>, - event_in_tx: mpsc::WeakSender, - event_in_rx: mpsc::Receiver, - event_out_tx: mpsc::Sender, - - /// Mapping between track SID and descriptor. - descriptors: HashMap, - /// Bidirectional mapping between track SID and subscriber handle. - sub_handles: HandleMap, -} - -impl ManagerTask { + /// Run the manager task, consuming self. + /// + /// The manager will continue running until receiving [`InputEvent::Shutdown`]. + /// pub async fn run(mut self) { log::debug!("Task started"); while let Some(event) = self.event_in_rx.recv().await { @@ -369,6 +336,51 @@ impl ManagerTask { } } +#[derive(Debug)] +struct Descriptor { + info: Arc, + state_tx: watch::Sender, + state: DescriptorState, +} + +#[derive(Debug)] +enum DescriptorState { + Available, + PendingSubscriberHandle { + result_txs: Vec>, + }, + Subscribed { + packet_tx: mpsc::Sender, + frame_tx: broadcast::Sender, + join_handle: livekit_runtime::JoinHandle<()>, + }, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum TrackState { + Published, + Unpublished, +} + +impl TrackState { + pub fn is_published(&self) -> bool { + !matches!(self, Self::Unpublished) + } +} + +/// Channel for sending [`InputEvent`]s to [`Manager`]. +#[derive(Debug, Clone)] +pub struct ManagerInput { + event_in_tx: mpsc::Sender, +} + +impl ManagerInput { + /// Sends an input event to the manager's task to be processed. + pub fn send(&self, event: InputEvent) -> Result<(), InternalError> { + Ok(self.event_in_tx.try_send(event).context("Failed to send input event")?) + } +} + #[cfg(test)] mod tests { use super::*; @@ -384,10 +396,10 @@ mod tests { #[tokio::test] async fn test_task_shutdown() { let options = ManagerOptions { decryption: None }; - let (manager, manager_task, _) = Manager::new(options); + let (manager, input, _) = Manager::new(options); - let join_handle = livekit_runtime::spawn(manager_task.run()); - _ = manager.send(InputEvent::Shutdown); + let join_handle = livekit_runtime::spawn(manager.run()); + _ = input.send(InputEvent::Shutdown); time::timeout(Duration::from_secs(1), join_handle).await.unwrap(); } @@ -400,8 +412,8 @@ mod tests { let sub_handle: Handle = Faker.fake(); let options = ManagerOptions { decryption: None }; - let (manager, manager_task, mut output_events) = Manager::new(options); - livekit_runtime::spawn(manager_task.run()); + let (manager, input, mut output) = Manager::new(options); + livekit_runtime::spawn(manager.run()); // Simulate track published let event = PublicationUpdatesEvent { @@ -415,10 +427,10 @@ mod tests { }], )]), }; - _ = manager.send(event.into()); + _ = input.send(event.into()); let wait_for_track = async { - while let Some(event) = output_events.next().await { + while let Some(event) = output.next().await { match event { OutputEvent::TrackAvailable(track) => return track, _ => continue, @@ -434,7 +446,7 @@ mod tests { assert_eq!(track.publisher_identity(), publisher_identity); let simulate_subscriber_handles = async { - while let Some(event) = output_events.next().await { + while let Some(event) = output.next().await { match event { OutputEvent::SubscriptionUpdated(event) => { assert!(event.subscribe); @@ -448,7 +460,7 @@ mod tests { track_sid.clone() )]), }; - _ = manager.send(event.into()); + _ = input.send(event.into()); } _ => {} } diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 3e618da7d..ce74ac6aa 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -380,7 +380,7 @@ struct SessionInner { // Data track managers local_dt_input: dt::local::ManagerInput, - remote_dt_manager: dt::remote::Manager, + remote_dt_input: dt::remote::ManagerInput, } /// Information about the local participant needed for outgoing @@ -498,10 +498,10 @@ impl RtcSession { .filter(|m| m.enabled()) .map(|m| Arc::new(m) as Arc), }; - let (remote_dt_manager, remote_dt_task, remote_dt_events) = + let (remote_dt_manager, remote_dt_input, remote_dt_output) = dt::remote::Manager::new(remote_dt_options); if let Ok(initial_publications) = dt::remote::event_from_join(&mut join_response) { - _ = remote_dt_manager.send(initial_publications.into()); + _ = remote_dt_input.send(initial_publications.into()); } let (close_tx, close_rx) = watch::channel(false); @@ -537,7 +537,7 @@ impl RtcSession { pending_requests: Default::default(), e2ee_manager, local_dt_input, - remote_dt_manager, + remote_dt_input }); // Start session tasks @@ -553,8 +553,8 @@ impl RtcSession { let local_dt_task = livekit_runtime::spawn(local_dt_manager.run()); let remote_dt_forward_task = - livekit_runtime::spawn(inner.clone().remote_dt_forward_task(remote_dt_events)); - let remote_dt_task = livekit_runtime::spawn(remote_dt_task.run()); + livekit_runtime::spawn(inner.clone().remote_dt_forward_task(remote_dt_output)); + let remote_dt_task = livekit_runtime::spawn(remote_dt_manager.run()); // TODO: closure. @@ -615,7 +615,7 @@ impl RtcSession { let _ = handle.local_dt_task.await; let _ = handle.local_dt_forward_task.await; - _ = self.inner.remote_dt_manager.send(dt::remote::InputEvent::Shutdown); + _ = self.inner.remote_dt_input.send(dt::remote::InputEvent::Shutdown); let _ = handle.remote_dt_task.await; let _ = handle.remote_dt_forward_task.await; } @@ -1106,7 +1106,7 @@ impl SessionInner { &mut update, local_participant_identity, ) { - _ = self.remote_dt_manager.send(event.into()); + _ = self.remote_dt_input.send(event.into()); } let _ = self .emitter @@ -1160,7 +1160,7 @@ impl SessionInner { } proto::signal_response::Message::DataTrackSubscriberHandles(subscriber_handles) => { let event: dt::remote::SubscriberHandlesEvent = subscriber_handles.try_into()?; - _ = self.remote_dt_manager.send(event.into()); + _ = self.remote_dt_input.send(event.into()); } proto::signal_response::Message::RefreshToken(ref token) => { let url = self.signal_client.url(); @@ -1211,7 +1211,7 @@ impl SessionInner { LOSSY_DC_LABEL => &self.sub_lossy_dc, RELIABLE_DC_LABEL => &self.sub_reliable_dc, DATA_TRACK_DC_LABEL => { - handle_remote_dt_packets(&data_channel, self.remote_dt_manager.clone()); + handle_remote_dt_packets(&data_channel, self.remote_dt_input.clone()); &self.sub_dt_transport } _ => return Ok(()), @@ -1996,7 +1996,7 @@ impl SessionInner { } /// Forward remote data track packets to the remote track manager. -pub fn handle_remote_dt_packets(dc: &DataChannel, manager: dt::remote::Manager) { +pub fn handle_remote_dt_packets(dc: &DataChannel, manager: dt::remote::ManagerInput) { let on_message: libwebrtc::data_channel::OnMessage = Box::new(move |buffer: DataBuffer| { if !buffer.binary { log::error!("Received non-binary message"); From 6b849df23f66e75d58248a1f36c3bbda6a38da16 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:31:24 +1100 Subject: [PATCH 153/232] Consistency --- livekit-datatrack/src/local/manager.rs | 33 ++++++++++---------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index f511d9afe..46c74c0c8 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -121,20 +121,20 @@ pub struct Manager { impl Manager { - /// Creates a new manager task. + /// Creates a new manager. /// /// Returns a tuple containing the following: /// - /// - The manager task itself to be spawned by the caller (see [`Manager::run`]). - /// - Channel for sending [`InputEvent`]s to be processed by the manager task. - /// - Stream for receiving [`OutputEvent`]s produced by the manager task. + /// - The manager itself to be spawned by the caller (see [`Manager::run`]). + /// - Channel for sending [`InputEvent`]s to be processed by the manager. + /// - Stream for receiving [`OutputEvent`]s produced by the manager. /// pub fn new(options: ManagerOptions) -> (Self, ManagerInput, impl Stream) { let (event_in_tx, event_in_rx) = mpsc::channel(4); // TODO: tune buffer size - let (event_out_tx, signal_out_rx) = mpsc::channel(4); + let (event_out_tx, event_out_rx) = mpsc::channel(4); let event_in = ManagerInput { event_in_tx: event_in_tx.clone() }; - let task = Manager { + let manager = Manager { encryption: options.encryption, event_in_tx: event_in_tx.downgrade(), event_in_rx, @@ -143,8 +143,8 @@ impl Manager { descriptors: HashMap::new(), }; - let event_out_stream = ReceiverStream::new(signal_out_rx); - (task, event_in, event_out_stream) + let event_out = ReceiverStream::new(event_out_rx); + (manager, event_in, event_out) } /// Run the manager task, consuming self. @@ -155,24 +155,17 @@ impl Manager { log::debug!("Task started"); while let Some(event) = self.event_in_rx.recv().await { log::debug!("Input event: {:?}", event); - if matches!(event, InputEvent::Shutdown) { - break; + match event { + InputEvent::Publish(event) => self.handle_publish(event), + InputEvent::PublishResult(event) => self.handle_publish_result(event), + InputEvent::Unpublish(event) => self.handle_unpublished(event), + InputEvent::Shutdown => break, } - self.handle_event(event); } self.shutdown().await; log::debug!("Task ended"); } - fn handle_event(&mut self, event: InputEvent) { - match event { - InputEvent::Publish(event) => self.handle_publish(event), - InputEvent::PublishResult(event) => self.handle_publish_result(event), - InputEvent::Unpublish(event) => self.handle_unpublished(event), - _ => {} - } - } - fn handle_publish(&mut self, event: PublishEvent) { let Some(handle) = self.handle_allocator.get() else { _ = event.result_tx.send(Err(PublishError::LimitReached)); From 25c3262c2a100fef1a5c97de7001c49525939526 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:35:40 +1100 Subject: [PATCH 154/232] Doc --- livekit-datatrack/src/dtp/handle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit-datatrack/src/dtp/handle.rs b/livekit-datatrack/src/dtp/handle.rs index c270a92ca..90363be46 100644 --- a/livekit-datatrack/src/dtp/handle.rs +++ b/livekit-datatrack/src/dtp/handle.rs @@ -15,7 +15,7 @@ use std::fmt::Display; use thiserror::Error; -/// Handle identifying a data track at the transport level. +/// Value identifying which data track a packet belongs to. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Handle(u16); From e0298bc7b829e54c53fc451d70b06f32b993a7de Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 11:00:11 +1100 Subject: [PATCH 155/232] Fix handle allocator --- livekit-datatrack/src/dtp/handle.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/livekit-datatrack/src/dtp/handle.rs b/livekit-datatrack/src/dtp/handle.rs index 90363be46..2b1372d06 100644 --- a/livekit-datatrack/src/dtp/handle.rs +++ b/livekit-datatrack/src/dtp/handle.rs @@ -76,8 +76,8 @@ pub struct HandleAllocator { impl HandleAllocator { /// Returns a unique track handle for the next publication, if one can be obtained. pub fn get(&mut self) -> Option { - let value = self.value.checked_add(1)?; - Handle(value).into() + self.value = self.value.checked_add(1)?; + Handle(self.value).into() } } From d072840cc9786d124fba1af3b90b7efa4eed4ecd Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 11:27:32 +1100 Subject: [PATCH 156/232] Test publish many tracks --- livekit/tests/data_track_test.rs | 44 ++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index f826bf7d3..6e7e59bd3 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -17,8 +17,8 @@ use { anyhow::{anyhow, Ok, Result}, common::test_rooms, futures_util::StreamExt, - livekit::{data_track::DataTrackOptions, RoomEvent}, - std::time::Duration, + livekit::prelude::*, + std::time::{Instant, Duration}, test_case::test_case, tokio::{ time::{self, timeout}, @@ -111,3 +111,43 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { .await??; Ok(()) } + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_publish_many_tracks() -> Result<()> { + const TRACK_COUNT: usize = 256; + + let (room, _) = test_rooms(1).await?.pop().unwrap(); + + let publish_tracks = async { + let mut tracks = Vec::with_capacity(TRACK_COUNT); + let start = Instant::now(); + + for idx in 0..TRACK_COUNT { + let name = format!("track_{}", idx); + let options = DataTrackOptions::with_name(name.clone()); + let track = room.local_participant().publish_data_track(options).await?; + + assert!(track.is_published()); + assert_eq!(track.info().name(), name); + + tracks.push(track); + } + + let elapsed = start.elapsed(); + log::info!( + "Publishing {} tracks took {:.2?} (average {:.2?} per track)", + TRACK_COUNT, + elapsed, + elapsed / TRACK_COUNT as u32 + ); + Ok(tracks) + }; + + let tracks = timeout(Duration::from_secs(5), publish_tracks).await??; + for track in &tracks { + // Publish a single large frame per track. + track.publish(vec![0xFA; 196_608].into())?; + } + Ok(()) +} From b4135b4f9c2b7180c2756a6824a6b7b3bd69ec8a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 11:39:10 +1100 Subject: [PATCH 157/232] Test publish unauthorized --- livekit/tests/common/e2e/mod.rs | 8 +++++--- livekit/tests/data_track_test.rs | 23 +++++++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/livekit/tests/common/e2e/mod.rs b/livekit/tests/common/e2e/mod.rs index fc0834847..b97e242ea 100644 --- a/livekit/tests/common/e2e/mod.rs +++ b/livekit/tests/common/e2e/mod.rs @@ -44,12 +44,12 @@ impl TestEnvironment { } } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct TestRoomOptions { /// Grants for the generated token. - grants: VideoGrants, + pub grants: VideoGrants, /// Options used for creating the [`Room`]. - room: RoomOptions, + pub room: RoomOptions, } impl Default for TestRoomOptions { @@ -90,6 +90,8 @@ pub async fn test_rooms_with_options( .enumerate() .map(|(id, mut options)| -> Result<(String, RoomOptions)> { options.grants.room = room_name.clone(); + + log::info!("{:?}", options); let token = AccessToken::with_api_key(&test_env.api_key, &test_env.api_secret) .with_ttl(Duration::from_secs(30 * 60)) // 30 minutes .with_grants(options.grants) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 6e7e59bd3..0fadf2dc3 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -15,10 +15,11 @@ #[cfg(feature = "__lk-e2e-test")] use { anyhow::{anyhow, Ok, Result}, - common::test_rooms, + common::{test_rooms, test_rooms_with_options, TestRoomOptions}, futures_util::StreamExt, livekit::prelude::*, - std::time::{Instant, Duration}, + livekit_api::access_token::VideoGrants, + std::time::{Duration, Instant}, test_case::test_case, tokio::{ time::{self, timeout}, @@ -151,3 +152,21 @@ async fn test_publish_many_tracks() -> Result<()> { } Ok(()) } + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_publish_unauthorized() -> Result<()> { + let (room, _) = test_rooms_with_options([TestRoomOptions { + grants: VideoGrants { room_join: true, can_publish_data: false, ..Default::default() }, + ..Default::default() + }]) + .await? + .pop() + .unwrap(); + + let options = DataTrackOptions::with_name("my_track"); + let result = room.local_participant().publish_data_track(options).await; + assert!(matches!(result, Err(PublishError::NotAllowed))); + + Ok(()) +} From 8f50b26f44ad44730a2f07ead1b9bb7b0096aff3 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 12:09:41 +1100 Subject: [PATCH 158/232] Test publish duplicate name --- livekit/tests/data_track_test.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 0fadf2dc3..aee6f986c 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -170,3 +170,17 @@ async fn test_publish_unauthorized() -> Result<()> { Ok(()) } + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_publish_duplicate_name() -> Result<()> { + let (room, _) = test_rooms(1).await?.pop().unwrap(); + + #[allow(unused)] + let first = room.local_participant().publish_data_track(DataTrackOptions::with_name("first")).await?; + + let second_result = room.local_participant().publish_data_track(DataTrackOptions::with_name("first")).await; + assert!(matches!(second_result, Err(PublishError::DuplicateName))); + + Ok(()) +} \ No newline at end of file From 3fa6217b34f55a933b68d6636ff4177dbd33fbec Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 12:27:39 +1100 Subject: [PATCH 159/232] More idiomatic track publish API --- examples/basic_data_track/src/publisher.rs | 10 +++----- livekit-datatrack/src/local/mod.rs | 23 +++++++++++++++---- .../src/room/participant/local_participant.rs | 4 ++-- livekit/tests/data_track_test.rs | 14 +++++------ 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/examples/basic_data_track/src/publisher.rs b/examples/basic_data_track/src/publisher.rs index 83d5a0eaa..d6ce29771 100644 --- a/examples/basic_data_track/src/publisher.rs +++ b/examples/basic_data_track/src/publisher.rs @@ -12,8 +12,7 @@ async fn main() -> Result<()> { let (room, _) = Room::connect(&url, &token, RoomOptions::default()).await?; - let options = DataTrackOptions::with_name("my_sensor_data"); - let track = room.local_participant().publish_data_track(options).await?; + let track = room.local_participant().publish_data_track("my_sensor_data").await?; tokio::select! { _ = publish_frames(track) => {} @@ -31,10 +30,7 @@ async fn publish_frames(track: LocalDataTrack) { loop { log::info!("Publishing frame"); let frame = read_sensor().await.into(); - track - .publish(frame) - .inspect_err(|err| println!("Failed to publish frame: {}", err)) - .ok(); + track.publish(frame).inspect_err(|err| println!("Failed to publish frame: {}", err)).ok(); time::sleep(Duration::from_millis(500)).await } -} \ No newline at end of file +} diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index a63b26b04..0b48b2b8d 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -143,8 +143,8 @@ impl PublishFrameError { /// Create options for publishing a track named "my_track" with end-to-end encryption disabled: /// ``` /// # use livekit_datatrack::api::DataTrackOptions; -/// let options = DataTrackOptions::with_name("my_track") -/// .disable_e2ee(true); +/// let options = DataTrackOptions::new("my_track") +/// .disable_e2ee(true); // Set additional options as needed /// ``` /// #[derive(Clone, Debug)] @@ -162,7 +162,7 @@ impl DataTrackOptions { /// - Must not be empty /// - Must be unique per publisher /// - pub fn with_name(name: impl Into) -> Self { + pub fn new(name: impl Into) -> Self { Self { name: name.into(), disable_e2ee: false } } @@ -170,8 +170,21 @@ impl DataTrackOptions { /// /// By default, room settings are used. /// - pub fn disable_e2ee(self, disabled: bool) -> Self { - Self { disable_e2ee: disabled, ..self } + pub fn disable_e2ee(mut self, disabled: bool) -> Self { + self.disable_e2ee = disabled; + self + } +} + +impl From for DataTrackOptions { + fn from(name: String) -> Self { + Self::new(name) + } +} + +impl From<&str> for DataTrackOptions { + fn from(name: &str) -> Self { + Self::new(name.to_string()) } } diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 016ce7bfd..c5b012946 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -231,9 +231,9 @@ impl LocalParticipant { /// Publish a data track. pub async fn publish_data_track( &self, - options: DataTrackOptions, + options: impl Into, ) -> Result, data_track::PublishError> { - self.inner.rtc_engine.publish_data_track(options).await + self.inner.rtc_engine.publish_data_track(options.into()).await } /// Publish a media track. diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index aee6f986c..da100def7 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -53,7 +53,7 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { let publish = async move { let track = pub_room .local_participant() - .publish_data_track(DataTrackOptions::with_name("my_track")) + .publish_data_track("my_track") .await?; log::info!("Track published"); @@ -126,8 +126,7 @@ async fn test_publish_many_tracks() -> Result<()> { for idx in 0..TRACK_COUNT { let name = format!("track_{}", idx); - let options = DataTrackOptions::with_name(name.clone()); - let track = room.local_participant().publish_data_track(options).await?; + let track = room.local_participant().publish_data_track(name.clone()).await?; assert!(track.is_published()); assert_eq!(track.info().name(), name); @@ -164,8 +163,7 @@ async fn test_publish_unauthorized() -> Result<()> { .pop() .unwrap(); - let options = DataTrackOptions::with_name("my_track"); - let result = room.local_participant().publish_data_track(options).await; + let result = room.local_participant().publish_data_track("my_track").await; assert!(matches!(result, Err(PublishError::NotAllowed))); Ok(()) @@ -177,10 +175,10 @@ async fn test_publish_duplicate_name() -> Result<()> { let (room, _) = test_rooms(1).await?.pop().unwrap(); #[allow(unused)] - let first = room.local_participant().publish_data_track(DataTrackOptions::with_name("first")).await?; + let first = room.local_participant().publish_data_track("first").await?; - let second_result = room.local_participant().publish_data_track(DataTrackOptions::with_name("first")).await; + let second_result = room.local_participant().publish_data_track("first").await; assert!(matches!(second_result, Err(PublishError::DuplicateName))); Ok(()) -} \ No newline at end of file +} From d342cc93acd6ed0e05f787d930130bef4f5104dc Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 12:40:53 +1100 Subject: [PATCH 160/232] Doc --- livekit-datatrack/src/frame.rs | 27 ++++++++++-------- livekit-datatrack/src/local/mod.rs | 44 ++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/livekit-datatrack/src/frame.rs b/livekit-datatrack/src/frame.rs index 7feca350b..fc4da3d01 100644 --- a/livekit-datatrack/src/frame.rs +++ b/livekit-datatrack/src/frame.rs @@ -15,7 +15,20 @@ use bytes::Bytes; use core::fmt; -/// Frame published over a data track containing a payload and optional metadata. +/// A frame published on a data track, consisting of a payload and optional metadata. +/// +/// # Examples +/// +/// Create a frame from a [`Vec`] payload: +/// +/// ``` +/// # use livekit_datatrack::api::DataTrackFrame; +/// let some_payload = vec![0xFA; 256]; +/// let frame: DataTrackFrame = some_payload.into(); +/// +/// assert_eq!(frame.payload().len(), 256); +/// ``` +/// #[derive(Clone, Default)] pub struct DataTrackFrame { pub(crate) payload: Bytes, @@ -35,21 +48,13 @@ impl DataTrackFrame { } impl DataTrackFrame { - /// Creates a data track frame with the given payload. - /// - /// # Examples - /// - /// ``` - /// # use livekit_datatrack::api::DataTrackFrame; - /// let frame = DataTrackFrame::new(vec![0xFA; 256]); - /// ``` - /// + /// Creates a frame from the given payload. pub fn new(payload: impl Into) -> Self { Self { payload: payload.into(), ..Default::default() } } /// Associates a user timestamp with the frame. - pub fn with_user_timestamp(&mut self, value: u64) -> &mut Self { + pub fn with_user_timestamp(mut self, value: u64) -> Self { self.user_timestamp = Some(value); self } diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 0b48b2b8d..cbdfca1d3 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -140,6 +140,8 @@ impl PublishFrameError { /// Options for publishing a data track. /// +/// # Examples +/// /// Create options for publishing a track named "my_track" with end-to-end encryption disabled: /// ``` /// # use livekit_datatrack::api::DataTrackOptions; @@ -188,23 +190,38 @@ impl From<&str> for DataTrackOptions { } } +/// An error that can occur when publishing a data track. #[derive(Debug, Error)] pub enum PublishError { - #[error("The local participant does not have permission to publish data tracks")] + /// Local participant does not have permission to publish data tracks. + /// + /// Ensure the participant's token contains the `canPublishData` grant. + /// + #[error("Data track publishing unauthorized")] NotAllowed, - #[error("A data track with the same name is already published by the local participant")] + + /// A track with the same name is already published by the local participant. + #[error("Track name already taken")] DuplicateName, - #[error("Request to publish data track timed-out")] + + /// Request to publish the track took long to complete. + #[error("Publish data track timed-out")] Timeout, - #[error("No more data tracks are able to be published")] + + /// No additional data tracks can be published by the local participant. + #[error("Data track publication limit reached")] LimitReached, - #[error("Cannot publish data track when disconnected")] + + /// Cannot publish data track when the room is disconnected. + #[error("Room disconnected")] Disconnected, + + /// Internal error, please report on GitHub. #[error(transparent)] Internal(#[from] InternalError), } -/// Frame could not be published to the track. +/// Frame could not be published to a data track. #[derive(Debug, Error)] #[error("Failed to publish frame: {reason}")] pub struct PublishFrameError { @@ -212,6 +229,21 @@ pub struct PublishFrameError { reason: PublishFrameErrorReason, } +impl PublishFrameError { + /// Returns the reason the frame could not be published. + pub fn reason(&self) -> PublishFrameErrorReason { + self.reason + } + + /// Consumes the error and returns the frame that couldn't be published. + /// + /// This may be useful for implementing application-specific retry logic. + /// + pub fn into_frame(self) -> DataTrackFrame { + self.frame + } +} + /// Reason why a data track frame could not be published. #[derive(Debug, Clone, Copy)] pub enum PublishFrameErrorReason { From 3dc8f5945f8274857d31b70060c7bdbc6b480f7a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:04:29 +1100 Subject: [PATCH 161/232] Remove duplication implementation --- livekit-datatrack/src/local/mod.rs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index cbdfca1d3..d89c48838 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -122,22 +122,6 @@ impl Drop for LocalTrackInner { } } -impl PublishFrameError { - pub(crate) fn new(frame: DataTrackFrame, reason: PublishFrameErrorReason) -> Self { - Self { frame, reason } - } - - /// Consume the error, returning the frame that couldn't be published. - pub fn into_frame(self) -> DataTrackFrame { - self.frame - } - - /// Returns the reason why the frame could not be published. - pub fn reason(&self) -> PublishFrameErrorReason { - self.reason - } -} - /// Options for publishing a data track. /// /// # Examples @@ -230,6 +214,10 @@ pub struct PublishFrameError { } impl PublishFrameError { + pub(crate) fn new(frame: DataTrackFrame, reason: PublishFrameErrorReason) -> Self { + Self { frame, reason } + } + /// Returns the reason the frame could not be published. pub fn reason(&self) -> PublishFrameErrorReason { self.reason From aa6418851ec2b18dc397e7e1265a2d81a6f99e43 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:12:37 +1100 Subject: [PATCH 162/232] Doc --- livekit-datatrack/src/local/mod.rs | 3 ++ livekit-datatrack/src/remote/mod.rs | 23 +++++++++- .../src/room/participant/local_participant.rs | 46 ++++++++++++++++++- 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index d89c48838..9f9347900 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -31,6 +31,9 @@ mod pipeline; pub type LocalDataTrack = DataTrack; /// Marker type indicating a [`DataTrack`] belongs to the local participant. +/// +/// See also: [`LocalDataTrack`] +/// #[derive(Debug)] pub struct Local; diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index cfa86e13d..bd9a2021a 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -32,6 +32,9 @@ mod pipeline; pub type RemoteDataTrack = DataTrack; /// Marker type indicating a [`DataTrack`] belongs to a remote participant. +/// +/// See also: [`RemoteDataTrack`] +/// #[derive(Debug, Clone)] pub struct Remote; @@ -49,7 +52,25 @@ impl DataTrack { } impl DataTrack { - /// Subscribe to the data track to receive frames. + /// Subscribes to the data track to receive frames. + /// + /// # Returns + /// + /// A stream that yields [`DataTrackFrame`]s as they arrive. + /// + /// # Multiple Subscriptions + /// + /// An application may call `subscribe` more than once to process frames in + /// multiple places. For example, one async task might plot values on a graph + /// while another writes them to a file. + /// + /// Internally, only the first call to `subscribe` communicates with the SFU and + /// allocates the resources required to receive frames. Additional subscriptions + /// reuse the same underlying pipeline and do not trigger additional signaling. + /// + /// Note that newly created subscriptions only receive frames published after + /// the subscription is established. + /// pub async fn subscribe(&self) -> Result, SubscribeError> { let (result_tx, result_rx) = oneshot::channel(); let subscribe_event = SubscribeEvent { sid: self.info.sid.clone(), result_tx }; diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index c5b012946..8c171f94c 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -228,7 +228,44 @@ impl LocalParticipant { vec } - /// Publish a data track. + /// Publishes a data track. + /// + /// # Returns + /// + /// The published data track if successful. Use [`LocalDataTrack::publish`] + /// to send data frames on the track. + /// + /// # Examples + /// + /// Publish a track named "my_track" using default options: + /// + /// ``` + /// # use livekit::prelude::*; + /// # async fn with_room(room: Room) -> Result<(), PublishError> { + /// let track = room + /// .local_participant() + /// .publish_data_track("my_track") + /// .await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// Publish a track named "my_track" with custom options: + /// + /// ``` + /// # use livekit::prelude::*; + /// # async fn with_room(room: Room) -> Result<(), PublishError> { + /// let options = DataTrackOptions::new("my_track") + /// .disable_e2ee(true); // Disable end-to-end encryption for this track + /// + /// let track = room + /// .local_participant() + /// .publish_data_track(options) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + /// pub async fn publish_data_track( &self, options: impl Into, @@ -236,7 +273,7 @@ impl LocalParticipant { self.inner.rtc_engine.publish_data_track(options.into()).await } - /// Publish a media track. + /// Publishes a media track. pub async fn publish_track( &self, track: LocalTrack, @@ -490,6 +527,11 @@ impl LocalParticipant { self.inner.rtc_engine.publish_data(packet, kind, true).await.map_err(Into::into) } + /// Publishes a data packet. + /// + /// This API will be deprecated in the near future. For new applications, + /// please consider using data tracks (see [`LocalParticipant::publish_data_track`]). + /// pub async fn publish_data(&self, packet: DataPacket) -> RoomResult<()> { let kind = match packet.reliable { true => DataPacketKind::Reliable, From 83c032c2305ad764a14ccc3ba4226dde3ea5ff6b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 14:12:54 +1100 Subject: [PATCH 163/232] Fix --- livekit-datatrack/src/local/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 46c74c0c8..ae5f8c386 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -410,7 +410,7 @@ mod tests { } }; let publish_track = async { - let track_options = DataTrackOptions::with_name(track_name.clone()); + let track_options = DataTrackOptions::new(track_name.clone()); let track = input.publish_track(track_options).await.unwrap(); assert!(!track.info().uses_e2ee()); assert_eq!(track.info().name(), track_name); From b1565d489ef99d19a781c6d9ea5ff6b1b7355609 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 14:13:05 +1100 Subject: [PATCH 164/232] Make crate internal --- livekit/src/room/e2ee/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit/src/room/e2ee/manager.rs b/livekit/src/room/e2ee/manager.rs index 9539293d5..5f03a05e4 100644 --- a/livekit/src/room/e2ee/manager.rs +++ b/livekit/src/room/e2ee/manager.rs @@ -247,7 +247,7 @@ impl E2eeManager { } /// Decrypt data received from a data channel - pub fn handle_encrypted_data( + pub(crate) fn handle_encrypted_data( &self, data: &[u8], iv: &[u8], From 07a596677580cedc666eb5b29d9635198655ee8a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:46:20 +1100 Subject: [PATCH 165/232] End to end encryption --- livekit-datatrack/src/e2ee.rs | 26 +++++---- livekit-datatrack/src/remote/manager.rs | 20 +++---- livekit-datatrack/src/remote/mod.rs | 2 +- livekit-datatrack/src/remote/pipeline.rs | 16 +++--- livekit/src/room/e2ee/data_track.rs | 68 ++++++++++++++++++++---- livekit/src/room/e2ee/manager.rs | 20 +++---- livekit/src/room/e2ee/mod.rs | 2 +- livekit/src/rtc_engine/rtc_session.rs | 56 +++++++++---------- 8 files changed, 131 insertions(+), 79 deletions(-) diff --git a/livekit-datatrack/src/e2ee.rs b/livekit-datatrack/src/e2ee.rs index 9db8e13bf..1f51e43cc 100644 --- a/livekit-datatrack/src/e2ee.rs +++ b/livekit-datatrack/src/e2ee.rs @@ -16,23 +16,31 @@ use bytes::Bytes; use core::fmt::Debug; use thiserror::Error; -#[derive(Debug, Error)] -#[error("End-to-end encryption failed")] -pub struct E2eeError; - +/// Encrypted payload and metadata required for decryption. pub struct EncryptedPayload { pub payload: Bytes, pub iv: [u8; 12], pub key_index: u8, } +#[derive(Debug, Error)] +#[error("Encryption failed")] +pub struct EncryptionError; + pub trait EncryptionProvider: Send + Sync + Debug { - /// Encrypt the given payload. - fn encrypt(&self, payload: Bytes) -> Result; + /// Encrypts the given payload being sent by the local participant. + fn encrypt(&self, payload: Bytes) -> Result; } +#[derive(Debug, Error)] +#[error("Decryption failed")] +pub struct DecryptionError; + pub trait DecryptionProvider: Send + Sync + Debug { - /// Decrypt the given payload. - fn decrypt(&self, payload: EncryptedPayload) -> Result; - // TODO: handle publisher identity + /// Decrypts the given payload received from a remote participant. + fn decrypt( + &self, + payload: EncryptedPayload, + sender_identity: &str, + ) -> Result; } diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index e285ee769..bcc03cda7 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -116,7 +116,6 @@ pub struct Manager { } impl Manager { - /// Creates a new manager. /// /// Returns a tuple containing the following: @@ -197,10 +196,15 @@ impl Manager { return; } let info = Arc::new(info); + let publisher_identity: Arc = publisher_identity.into(); let (state_tx, state_rx) = watch::channel(TrackState::Published); - let descriptor = - Descriptor { info: info.clone(), state_tx, state: DescriptorState::Available }; + let descriptor = Descriptor { + info: info.clone(), + publisher_identity: publisher_identity.clone(), + state_tx, + state: DescriptorState::Available, + }; self.descriptors.insert(descriptor.info.sid.clone(), descriptor); let inner = RemoteTrackInner { @@ -279,6 +283,7 @@ impl Manager { depacketizer: Depacketizer::new(), decryption: self.decryption.clone(), info: descriptor.info.clone(), + publisher_identity: descriptor.publisher_identity.clone(), state_rx: descriptor.state_tx.subscribe(), packet_rx, frame_tx: frame_tx.clone(), @@ -339,6 +344,7 @@ impl Manager { #[derive(Debug)] struct Descriptor { info: Arc, + publisher_identity: Arc, state_tx: watch::Sender, state: DescriptorState, } @@ -386,8 +392,7 @@ mod tests { use super::*; use fake::{ faker::{internet::en::SafeEmail, lorem::en::Word}, - Fake, - Faker + Fake, Faker, }; use futures_util::StreamExt; use std::{collections::HashMap, time::Duration}; @@ -455,10 +460,7 @@ mod tests { // Simulate SFU reply let event = SubscriberHandlesEvent { - mapping: HashMap::from([( - sub_handle, - track_sid.clone() - )]), + mapping: HashMap::from([(sub_handle, track_sid.clone())]), }; _ = input.send(event.into()); } diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index bd9a2021a..8d3efc0e0 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -112,7 +112,7 @@ impl DataTrack { #[derive(Debug, Clone)] pub(crate) struct RemoteTrackInner { - publisher_identity: String, + publisher_identity: Arc, state_rx: watch::Receiver, event_in_tx: mpsc::WeakSender, } diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 6c066fae8..00713374a 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -27,6 +27,7 @@ pub(super) struct RemoteTrackTask { pub depacketizer: Depacketizer, pub decryption: Option>, pub info: Arc, + pub publisher_identity: Arc, pub state_rx: watch::Receiver, pub packet_rx: mpsc::Receiver, pub frame_tx: broadcast::Sender, @@ -64,13 +65,14 @@ impl RemoteTrackTask { }; let encrypted_payload = EncryptedPayload { payload: frame.payload, iv: e2ee.iv, key_index: e2ee.key_index }; - let decrypted_payload = match decryption.decrypt(encrypted_payload) { - Ok(decrypted_payload) => decrypted_payload, - Err(err) => { - log::error!("Decryption failed: {}", err); - return; - } - }; + let decrypted_payload = + match decryption.decrypt(encrypted_payload, &self.publisher_identity) { + Ok(decrypted_payload) => decrypted_payload, + Err(err) => { + log::error!("{}", err); + return; + } + }; frame.payload = decrypted_payload; } _ = self.frame_tx.send(frame.into()); diff --git a/livekit/src/room/e2ee/data_track.rs b/livekit/src/room/e2ee/data_track.rs index e53cd1a80..4250c5763 100644 --- a/livekit/src/room/e2ee/data_track.rs +++ b/livekit/src/room/e2ee/data_track.rs @@ -12,19 +12,65 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::E2eeManager; -use livekit_datatrack::internal::{ - DecryptionProvider, E2eeError, EncryptedPayload, EncryptionProvider, -}; - -impl DecryptionProvider for E2eeManager { - fn decrypt(&self, payload: EncryptedPayload) -> Result { - todo!() +use crate::{id::ParticipantIdentity, E2eeManager}; +use bytes::Bytes; +use livekit_datatrack::internal as dt; + +/// Wrapper around [`E2eeManager`] implementing [`dt::EncryptionProvider`]. +#[derive(Debug)] +pub(crate) struct DataTrackEncryptionProvider { + manager: E2eeManager, + sender_identity: ParticipantIdentity, +} + +impl DataTrackEncryptionProvider { + pub fn new(manager: E2eeManager, sender_identity: ParticipantIdentity) -> Self { + Self { manager, sender_identity } + } +} + +impl dt::EncryptionProvider for DataTrackEncryptionProvider { + fn encrypt(&self, payload: bytes::Bytes) -> Result { + let encrypted = self + .manager + .encrypt_data(payload.into(), &self.sender_identity) + .map_err(|_| dt::EncryptionError)?; + + let payload = encrypted.data.into(); + let iv = encrypted.iv.try_into().map_err(|_| dt::EncryptionError)?; + let key_index = encrypted.key_index.try_into().map_err(|_| dt::EncryptionError)?; + + Ok(dt::EncryptedPayload { payload, iv, key_index }) + } +} + +/// Wrapper around [`E2eeManager`] implementing [`dt::DecryptionProvider`]. +#[derive(Debug)] +pub(crate) struct DataTrackDecryptionProvider { + manager: E2eeManager, +} + +impl DataTrackDecryptionProvider { + pub fn new(manager: E2eeManager) -> Self { + Self { manager } } } -impl EncryptionProvider for E2eeManager { - fn encrypt(&self, payload: bytes::Bytes) -> Result { - todo!() +impl dt::DecryptionProvider for DataTrackDecryptionProvider { + fn decrypt( + &self, + payload: dt::EncryptedPayload, + sender_identity: &str, + ) -> Result { + let decrypted = self + .manager + .decrypt_data( + payload.payload.into(), + payload.iv.to_vec(), + payload.key_index as u32, + sender_identity, + ) + .ok_or_else(|| dt::DecryptionError)?; + Ok(Bytes::from(decrypted)) } } diff --git a/livekit/src/room/e2ee/manager.rs b/livekit/src/room/e2ee/manager.rs index 5f03a05e4..741b3808f 100644 --- a/livekit/src/room/e2ee/manager.rs +++ b/livekit/src/room/e2ee/manager.rs @@ -247,19 +247,18 @@ impl E2eeManager { } /// Decrypt data received from a data channel - pub(crate) fn handle_encrypted_data( + pub(crate) fn decrypt_data( &self, - data: &[u8], - iv: &[u8], - participant_identity: &str, + data: Vec, + iv: Vec, key_index: u32, + participant_identity: &str, ) -> Option> { let inner = self.inner.lock(); let data_packet_cryptor = inner.data_packet_cryptor.as_ref()?; - let encrypted_packet = EncryptedPacket { data: data.to_vec(), iv: iv.to_vec(), key_index }; - + let encrypted_packet = EncryptedPacket { data, iv, key_index }; match data_packet_cryptor.decrypt(participant_identity, &encrypted_packet) { Ok(decrypted_data) => Some(decrypted_data), Err(e) => { @@ -272,16 +271,17 @@ impl E2eeManager { /// Encrypt data for transmission over a data channel pub(crate) fn encrypt_data( &self, - data: &[u8], - participant_identity: &str, - key_index: u32, + data: Vec, + participant_identity: &ParticipantIdentity, ) -> Result> { let inner = self.inner.lock(); + let key_index = self.key_provider().map_or(0, |kp| kp.get_latest_key_index() as u32); + let data_packet_cryptor = inner.data_packet_cryptor.as_ref().ok_or("DataPacketCryptor is not initialized")?; - data_packet_cryptor.encrypt(participant_identity, key_index, data) + data_packet_cryptor.encrypt(participant_identity.as_str(), key_index, &data) } } diff --git a/livekit/src/room/e2ee/mod.rs b/livekit/src/room/e2ee/mod.rs index 520959442..e1235d81d 100644 --- a/livekit/src/room/e2ee/mod.rs +++ b/livekit/src/room/e2ee/mod.rs @@ -20,7 +20,7 @@ pub mod key_provider; pub mod manager; /// Provider implementations for data track. -mod data_track; +pub(crate) mod data_track; #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum EncryptionType { diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index ce74ac6aa..6fa0b54c6 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -44,14 +44,7 @@ use tokio::sync::{mpsc, oneshot, watch, Notify}; use super::{rtc_events, EngineError, EngineOptions, EngineResult, SimulateScenario}; use crate::{ - id::ParticipantIdentity, - utils::{ - ttl_map::TtlMap, - tx_queue::{TxQueue, TxQueueItem}, - }, - ChatMessage, TranscriptionSegment, -}; -use crate::{ + e2ee::data_track::*, id::ParticipantSid, options::TrackPublishOptions, prelude::TrackKind, @@ -64,6 +57,14 @@ use crate::{ track::LocalTrack, DataPacketKind, }; +use crate::{ + id::ParticipantIdentity, + utils::{ + ttl_map::TtlMap, + tx_queue::{TxQueue, TxQueueItem}, + }, + ChatMessage, TranscriptionSegment, +}; pub const ICE_CONNECT_TIMEOUT: Duration = Duration::from_secs(15); pub const TRACK_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); @@ -484,19 +485,18 @@ impl RtcSession { rtc_events::forward_dc_events(&mut reliable_dc, DataPacketKind::Reliable, rtc_emitter); let local_dt_options = dt::local::ManagerOptions { - encryption: e2ee_manager - .clone() - .filter(|m| m.enabled()) - .map(|m| Arc::new(m) as Arc), + encryption: e2ee_manager.clone().filter(|m| m.enabled()).map(|m| { + Arc::new(DataTrackEncryptionProvider::new(m, participant_info.identity.clone())) + as Arc + }), }; let (local_dt_manager, local_dt_input, local_dt_output) = dt::local::Manager::new(local_dt_options); let remote_dt_options = dt::remote::ManagerOptions { - decryption: e2ee_manager - .clone() - .filter(|m| m.enabled()) - .map(|m| Arc::new(m) as Arc), + decryption: e2ee_manager.clone().filter(|m| m.enabled()).map(|m| { + Arc::new(DataTrackDecryptionProvider::new(m)) as Arc + }), }; let (remote_dt_manager, remote_dt_input, remote_dt_output) = dt::remote::Manager::new(remote_dt_options); @@ -537,7 +537,7 @@ impl RtcSession { pending_requests: Default::default(), e2ee_manager, local_dt_input, - remote_dt_input + remote_dt_input, }); // Start session tasks @@ -1390,14 +1390,15 @@ impl SessionInner { proto::data_packet::Value::EncryptedPacket(encrypted_packet) => { // Handle encrypted data packets if let Some(e2ee_manager) = &self.e2ee_manager { + let encryption_type = encrypted_packet.encryption_type(); let participant_identity_str = participant_identity.as_ref().map(|p| p.0.as_str()).unwrap_or(""); - match e2ee_manager.handle_encrypted_data( - &encrypted_packet.encrypted_value, - &encrypted_packet.iv, - participant_identity_str, + match e2ee_manager.decrypt_data( + encrypted_packet.encrypted_value, + encrypted_packet.iv, encrypted_packet.key_index, + participant_identity_str, ) { Some(decrypted_payload) => { // Parse the decrypted payload as EncryptedPacketPayload @@ -1410,7 +1411,7 @@ impl SessionInner { participant_sid, participant_identity, convert_encrypted_to_data_packet_value(decrypted_value), - encrypted_packet.encryption_type(), + encryption_type, ); Ok(()) } else { @@ -1684,15 +1685,8 @@ impl SessionInner { // Encode the payload and encrypt it let payload_bytes = encrypted_payload.encode_to_vec(); - let key_index = e2ee_manager - .key_provider() - .map(|kp| kp.get_latest_key_index() as u32) - .unwrap_or(0); - match e2ee_manager.encrypt_data( - &payload_bytes, - &self.participant_info.identity.0, - key_index, - ) { + match e2ee_manager.encrypt_data(payload_bytes, &self.participant_info.identity) + { Ok(encrypted_data) => { // Replace with EncryptedPacket variant packet.value = Some(proto::data_packet::Value::EncryptedPacket( From 276bbf630d650818dfc1a6c37691b731c11ff388 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:48:51 +1100 Subject: [PATCH 166/232] Format --- livekit-datatrack/src/remote/pipeline.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 00713374a..09e2fb885 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{depacketizer::Depacketizer, manager::TrackState}; +use super::{ + depacketizer::Depacketizer, + manager::{OutputEvent, TrackState}, +}; use crate::{ api::{DataTrackFrame, DataTrackInfo}, dtp::Dtp, @@ -31,7 +34,7 @@ pub(super) struct RemoteTrackTask { pub state_rx: watch::Receiver, pub packet_rx: mpsc::Receiver, pub frame_tx: broadcast::Sender, - pub event_out_tx: mpsc::WeakSender, + pub event_out_tx: mpsc::WeakSender, } impl RemoteTrackTask { From 736cc60cea8cfeee1fefb644474741badbbbc514 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:48:56 +1100 Subject: [PATCH 167/232] Doc --- livekit-datatrack/src/local/pipeline.rs | 2 +- livekit-datatrack/src/remote/depacketizer.rs | 2 +- livekit-datatrack/src/remote/pipeline.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index db6450b96..808b5caf8 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -38,7 +38,7 @@ impl LocalTrackTask { let mut state = *self.state_rx.borrow(); while state.is_published() { tokio::select! { - biased; + biased; // State updates take priority _ = self.state_rx.changed() => { state = *self.state_rx.borrow(); }, diff --git a/livekit-datatrack/src/remote/depacketizer.rs b/livekit-datatrack/src/remote/depacketizer.rs index 59fd02dd0..3b134dd8d 100644 --- a/livekit-datatrack/src/remote/depacketizer.rs +++ b/livekit-datatrack/src/remote/depacketizer.rs @@ -16,7 +16,7 @@ use crate::dtp::{Dtp, Extensions, FrameMarker}; use bytes::{Bytes, BytesMut}; use std::collections::BTreeMap; -/// Assembles packets into frames. +/// Reassembles packets into frames. #[derive(Debug)] pub struct Depacketizer { /// Partial frame currently being assembled. diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 09e2fb885..51eba2409 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -43,7 +43,7 @@ impl RemoteTrackTask { let mut state = *self.state_rx.borrow(); while state.is_published() { tokio::select! { - biased; + biased; // State updates take priority _ = self.state_rx.changed() => { state = *self.state_rx.borrow(); }, From b29c7c05edbcf3edb2acfdae92b2dc42ead1dec1 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:58:38 +1100 Subject: [PATCH 168/232] Fix deadlock --- livekit/src/room/e2ee/data_track.rs | 7 ++++++- livekit/src/room/e2ee/manager.rs | 3 +-- livekit/src/rtc_engine/rtc_session.rs | 7 ++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/livekit/src/room/e2ee/data_track.rs b/livekit/src/room/e2ee/data_track.rs index 4250c5763..8c86c0823 100644 --- a/livekit/src/room/e2ee/data_track.rs +++ b/livekit/src/room/e2ee/data_track.rs @@ -31,9 +31,14 @@ impl DataTrackEncryptionProvider { impl dt::EncryptionProvider for DataTrackEncryptionProvider { fn encrypt(&self, payload: bytes::Bytes) -> Result { + let key_index = self + .manager + .key_provider() + .map_or(0, |kp| kp.get_latest_key_index() as u32); + let encrypted = self .manager - .encrypt_data(payload.into(), &self.sender_identity) + .encrypt_data(payload.into(), &self.sender_identity, key_index) .map_err(|_| dt::EncryptionError)?; let payload = encrypted.data.into(); diff --git a/livekit/src/room/e2ee/manager.rs b/livekit/src/room/e2ee/manager.rs index 741b3808f..47ade5c75 100644 --- a/livekit/src/room/e2ee/manager.rs +++ b/livekit/src/room/e2ee/manager.rs @@ -273,11 +273,10 @@ impl E2eeManager { &self, data: Vec, participant_identity: &ParticipantIdentity, + key_index: u32 ) -> Result> { let inner = self.inner.lock(); - let key_index = self.key_provider().map_or(0, |kp| kp.get_latest_key_index() as u32); - let data_packet_cryptor = inner.data_packet_cryptor.as_ref().ok_or("DataPacketCryptor is not initialized")?; diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 6fa0b54c6..ea301f491 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -1685,7 +1685,12 @@ impl SessionInner { // Encode the payload and encrypt it let payload_bytes = encrypted_payload.encode_to_vec(); - match e2ee_manager.encrypt_data(payload_bytes, &self.participant_info.identity) + + let key_index = e2ee_manager + .key_provider() + .map_or(0, |kp| kp.get_latest_key_index() as u32); + + match e2ee_manager.encrypt_data(payload_bytes, &self.participant_info.identity, key_index) { Ok(encrypted_data) => { // Replace with EncryptedPacket variant From 54cf8b49368417265f850580830cec976f53666e Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:18:58 +1100 Subject: [PATCH 169/232] Remove log --- livekit/tests/common/e2e/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/livekit/tests/common/e2e/mod.rs b/livekit/tests/common/e2e/mod.rs index b97e242ea..6ff1c84d8 100644 --- a/livekit/tests/common/e2e/mod.rs +++ b/livekit/tests/common/e2e/mod.rs @@ -91,7 +91,6 @@ pub async fn test_rooms_with_options( .map(|(id, mut options)| -> Result<(String, RoomOptions)> { options.grants.room = room_name.clone(); - log::info!("{:?}", options); let token = AccessToken::with_api_key(&test_env.api_key, &test_env.api_secret) .with_ttl(Duration::from_secs(30 * 60)) // 30 minutes .with_grants(options.grants) From 63f17e275975c00370efc2440357e848b22f33b4 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:47:01 +1100 Subject: [PATCH 170/232] Test end-to-end encryption --- livekit/tests/data_track_test.rs | 79 ++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index da100def7..4b6a1b2b3 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -51,10 +51,7 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { log::info!("Publishing {} frames", frame_count); let publish = async move { - let track = pub_room - .local_participant() - .publish_data_track("my_track") - .await?; + let track = pub_room.local_participant().publish_data_track("my_track").await?; log::info!("Track published"); assert!(track.is_published()); @@ -182,3 +179,77 @@ async fn test_publish_duplicate_name() -> Result<()> { Ok(()) } + +#[cfg(feature = "__lk-e2e-test")] +#[test_case(false; "use_room_settings")] +#[test_case(true; "disabled_on_track")] +#[test_log::test(tokio::test)] +async fn test_e2ee(disable_on_track: bool) -> Result<()> { + use livekit::e2ee::{ + key_provider::{KeyProvider, KeyProviderOptions}, + EncryptionType, + }; + use livekit::E2eeOptions; + + const SHARED_SECRET: &[u8] = b"password"; + + let key_provider1 = + KeyProvider::with_shared_key(KeyProviderOptions::default(), SHARED_SECRET.to_vec()); + + let mut options1 = RoomOptions::default(); + options1.encryption = + Some(E2eeOptions { key_provider: key_provider1, encryption_type: EncryptionType::Gcm }); + + let key_provider2 = + KeyProvider::with_shared_key(KeyProviderOptions::default(), SHARED_SECRET.to_vec()); + + let mut options2 = RoomOptions::default(); + options2.encryption = + Some(E2eeOptions { key_provider: key_provider2, encryption_type: EncryptionType::Gcm }); + + let mut rooms = test_rooms_with_options([options1.into(), options2.into()]).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (sub_room, mut sub_room_event_rx) = rooms.pop().unwrap(); + + pub_room.e2ee_manager().set_enabled(true); + sub_room.e2ee_manager().set_enabled(true); + + let publish = async move { + let options = DataTrackOptions::new("my_track").disable_e2ee(disable_on_track); + let track = pub_room.local_participant().publish_data_track(options).await?; + assert!(track.info().uses_e2ee() || disable_on_track); + + for index in 0..5 { + track.publish(vec![index as u8; 196_608].into())?; + time::sleep(Duration::from_millis(25)).await; + } + Ok(()) + }; + + let subscribe = async move { + let track = async move { + while let Some(event) = sub_room_event_rx.recv().await { + let RoomEvent::RemoteDataTrackPublished(track) = event else { + continue; + }; + return Ok(track); + } + Err(anyhow!("No track published")) + } + .await?; + + assert!(track.info().uses_e2ee() || disable_on_track); + let mut subscription = track.subscribe().await?; + + while let Some(frame) = subscription.next().await { + let payload = frame.payload(); + if let Some(first_byte) = payload.first() { + assert!(payload.iter().all(|byte| byte == first_byte)); + } + } + Ok(()) + }; + timeout(Duration::from_secs(5), async { try_join!(publish, subscribe) }).await??; + Ok(()) +} From af12b5510c19ed91fa948e16379fde4c40178b94 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:47:39 +1100 Subject: [PATCH 171/232] Fix disable e2ee per track, rename --- livekit-datatrack/src/local/manager.rs | 43 +++++++++++++----------- livekit-datatrack/src/local/pipeline.rs | 6 ++-- livekit-datatrack/src/remote/manager.rs | 17 ++++++---- livekit-datatrack/src/remote/pipeline.rs | 4 +-- livekit/src/rtc_engine/rtc_session.rs | 4 +-- 5 files changed, 42 insertions(+), 32 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index ae5f8c386..4c8919acf 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -106,12 +106,12 @@ pub struct ManagerOptions { /// /// If none, end-to-end encryption will be disabled for all published tracks. /// - pub encryption: Option>, + pub e2ee_provider: Option>, } /// System for managing data track publications. pub struct Manager { - encryption: Option>, + e2ee_provider: Option>, event_in_tx: mpsc::WeakSender, event_in_rx: mpsc::Receiver, event_out_tx: mpsc::Sender, @@ -120,7 +120,6 @@ pub struct Manager { } impl Manager { - /// Creates a new manager. /// /// Returns a tuple containing the following: @@ -135,7 +134,7 @@ impl Manager { let event_in = ManagerInput { event_in_tx: event_in_tx.clone() }; let manager = Manager { - encryption: options.encryption, + e2ee_provider: options.e2ee_provider, event_in_tx: event_in_tx.downgrade(), event_in_rx, event_out_tx, @@ -183,7 +182,7 @@ impl Manager { let publish_requested = PublishRequestEvent { handle, name: event.options.name, - uses_e2ee: self.encryption.is_some() && !event.options.disable_e2ee, + uses_e2ee: self.e2ee_provider.is_some() && !event.options.disable_e2ee, }; _ = self.event_out_tx.try_send(publish_requested.into()); // TODO: check for error. self.schedule_publish_timeout(handle); @@ -203,11 +202,11 @@ impl Manager { fn handle_publish_result(&mut self, event: PublishResultEvent) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { log::warn!("No descriptor for {}", event.handle); - return + return; }; let Descriptor::Pending(result_tx) = descriptor else { log::warn!("Track {} already active", event.handle); - return + return; }; if result_tx.is_closed() { @@ -222,17 +221,25 @@ impl Manager { let (state_tx, state_rx) = watch::channel(LocalTrackState::Published); let info = Arc::new(info); + let e2ee_provider = if info.uses_e2ee() { + self.e2ee_provider.as_ref().map(Arc::clone) + } else { + None + }; let task = LocalTrackTask { // TODO: handle cancellation packetizer: Packetizer::new(info.pub_handle, Self::TRANSPORT_MTU), - encryption: self.encryption.clone(), + e2ee_provider, info: info.clone(), frame_rx, state_rx, event_out_tx: self.event_out_tx.clone(), }; let join_handle = livekit_runtime::spawn(task.run()); - self.descriptors.insert(info.pub_handle, Descriptor::Active { state_tx: state_tx.clone(), join_handle }); + self.descriptors.insert( + info.pub_handle, + Descriptor::Active { state_tx: state_tx.clone(), join_handle }, + ); let inner = LocalTrackInner { frame_tx, state_tx }; LocalDataTrack::new(info, inner) @@ -241,17 +248,16 @@ impl Manager { fn handle_unpublished(&mut self, event: UnpublishEvent) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { log::warn!("No descriptor for track {}", event.handle); - return + return; }; let Descriptor::Active { state_tx, .. } = descriptor else { log::warn!("Cannot unpublish pending track {}", event.handle); - return + return; }; if !state_tx.borrow().is_published() { - return + return; } - _ = state_tx - .send(LocalTrackState::Unpublished { initiator: UnpublishInitiator::Sfu }); + _ = state_tx.send(LocalTrackState::Unpublished { initiator: UnpublishInitiator::Sfu }); } /// Performs cleanup before the task ends. @@ -292,7 +298,7 @@ enum Descriptor { /// Active { state_tx: watch::Sender, - join_handle: livekit_runtime::JoinHandle<()> + join_handle: livekit_runtime::JoinHandle<()>, }, } @@ -322,7 +328,6 @@ pub struct ManagerInput { } impl ManagerInput { - /// Sends an input event to the manager's task to be processed. pub fn send(&self, event: InputEvent) -> Result<(), InternalError> { Ok(self.event_in_tx.try_send(event).context("Failed to handle input event")?) @@ -347,13 +352,13 @@ impl ManagerInput { mod tests { use super::*; use crate::{api::DataTrackSid, dtp::Dtp}; + use fake::{faker::lorem::en::Word, Fake, Faker}; use futures_util::StreamExt; use livekit_runtime::sleep; - use fake::{Fake, Faker, faker::lorem::en::Word}; #[tokio::test] async fn test_task_shutdown() { - let options = ManagerOptions { encryption: None }; + let options = ManagerOptions { e2ee_provider: None }; let (manager, input, _) = Manager::new(options); let join_handle = livekit_runtime::spawn(manager.run()); @@ -371,7 +376,7 @@ mod tests { let track_sid: DataTrackSid = Faker.fake(); let pub_handle: Handle = Faker.fake(); - let options = ManagerOptions { encryption: None }; + let options = ManagerOptions { e2ee_provider: None }; let (manager, input, mut output) = Manager::new(options); livekit_runtime::spawn(manager.run()); diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 808b5caf8..7a063196f 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -25,7 +25,7 @@ use tokio::sync::{mpsc, watch}; /// Task responsible for publishing frames for an individual data track. pub(super) struct LocalTrackTask { pub packetizer: Packetizer, - pub encryption: Option>, + pub e2ee_provider: Option>, pub info: Arc, pub state_rx: watch::Receiver, pub frame_rx: mpsc::Receiver, @@ -57,9 +57,9 @@ impl LocalTrackTask { fn publish_frame(&mut self, mut frame: DataTrackFrame) { let mut e2ee: Option = None; - if let Some(encryption) = &self.encryption { + if let Some(e2ee_provider) = &self.e2ee_provider { debug_assert!(self.info.uses_e2ee); - let encrypted_payload = match encryption.encrypt(frame.payload) { + let encrypted_payload = match e2ee_provider.encrypt(frame.payload) { Ok(payload) => payload, Err(err) => { log::error!("Failed to encrypt frame: {}", err); diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index bcc03cda7..49eba6f09 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -99,12 +99,12 @@ pub struct ManagerOptions { /// If none, remote tracks using end-to-end encryption will not be available /// for subscription. /// - pub decryption: Option>, + pub e2ee_provider: Option>, } /// System for managing data track subscriptions. pub struct Manager { - decryption: Option>, + e2ee_provider: Option>, event_in_tx: mpsc::WeakSender, event_in_rx: mpsc::Receiver, event_out_tx: mpsc::Sender, @@ -130,7 +130,7 @@ impl Manager { let event_in = ManagerInput { event_in_tx: event_in_tx.clone() }; let manager = Manager { - decryption: options.decryption, + e2ee_provider: options.e2ee_provider, event_in_tx: event_in_tx.downgrade(), event_in_rx, event_out_tx, @@ -279,9 +279,14 @@ impl Manager { let (packet_tx, packet_rx) = mpsc::channel(4); // TODO: tune let (frame_tx, frame_rx) = broadcast::channel(4); + let e2ee_provider = if descriptor.info.uses_e2ee() { + self.e2ee_provider.as_ref().map(Arc::clone) + } else { + None + }; let track_task = RemoteTrackTask { depacketizer: Depacketizer::new(), - decryption: self.decryption.clone(), + e2ee_provider, info: descriptor.info.clone(), publisher_identity: descriptor.publisher_identity.clone(), state_rx: descriptor.state_tx.subscribe(), @@ -400,7 +405,7 @@ mod tests { #[tokio::test] async fn test_task_shutdown() { - let options = ManagerOptions { decryption: None }; + let options = ManagerOptions { e2ee_provider: None }; let (manager, input, _) = Manager::new(options); let join_handle = livekit_runtime::spawn(manager.run()); @@ -416,7 +421,7 @@ mod tests { let track_sid: DataTrackSid = Faker.fake(); let sub_handle: Handle = Faker.fake(); - let options = ManagerOptions { decryption: None }; + let options = ManagerOptions { e2ee_provider: None }; let (manager, input, mut output) = Manager::new(options); livekit_runtime::spawn(manager.run()); diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 51eba2409..ca042f6ec 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -28,7 +28,7 @@ use tokio::sync::{broadcast, mpsc, watch}; /// Task responsible for receiving frames for a subscribed data track. pub(super) struct RemoteTrackTask { pub depacketizer: Depacketizer, - pub decryption: Option>, + pub e2ee_provider: Option>, pub info: Arc, pub publisher_identity: Arc, pub state_rx: watch::Receiver, @@ -59,7 +59,7 @@ impl RemoteTrackTask { fn receive_packet(&mut self, dtp: Dtp) { let Some(mut frame) = self.depacketizer.push(dtp) else { return }; - if let Some(decryption) = &self.decryption { + if let Some(decryption) = &self.e2ee_provider { debug_assert!(self.info.uses_e2ee); let Some(e2ee) = frame.extensions.e2ee else { diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index ea301f491..efa7dea5d 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -485,7 +485,7 @@ impl RtcSession { rtc_events::forward_dc_events(&mut reliable_dc, DataPacketKind::Reliable, rtc_emitter); let local_dt_options = dt::local::ManagerOptions { - encryption: e2ee_manager.clone().filter(|m| m.enabled()).map(|m| { + e2ee_provider: e2ee_manager.clone().filter(|m| m.enabled()).map(|m| { Arc::new(DataTrackEncryptionProvider::new(m, participant_info.identity.clone())) as Arc }), @@ -494,7 +494,7 @@ impl RtcSession { dt::local::Manager::new(local_dt_options); let remote_dt_options = dt::remote::ManagerOptions { - decryption: e2ee_manager.clone().filter(|m| m.enabled()).map(|m| { + e2ee_provider: e2ee_manager.clone().filter(|m| m.enabled()).map(|m| { Arc::new(DataTrackDecryptionProvider::new(m)) as Arc }), }; From 0cc9c2217f5f215e351779c9e69b786575f6f57f Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:00:11 +1100 Subject: [PATCH 172/232] Rename to pipeline --- livekit-datatrack/src/local/manager.rs | 14 +++++++------- livekit-datatrack/src/local/pipeline.rs | 13 +++++++------ livekit-datatrack/src/remote/manager.rs | 15 +++++++-------- livekit-datatrack/src/remote/pipeline.rs | 7 ++++--- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 4c8919acf..f2304c5a2 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{packetizer::Packetizer, pipeline::LocalTrackTask, LocalTrackInner}; +use super::{packetizer::Packetizer, pipeline::Pipeline, LocalTrackInner}; use crate::{ api::{DataTrackInfo, DataTrackOptions, InternalError, PublishError}, dtp::{self, Handle}, @@ -226,7 +226,7 @@ impl Manager { } else { None }; - let task = LocalTrackTask { + let pipeline = Pipeline { // TODO: handle cancellation packetizer: Packetizer::new(info.pub_handle, Self::TRANSPORT_MTU), e2ee_provider, @@ -235,10 +235,10 @@ impl Manager { state_rx, event_out_tx: self.event_out_tx.clone(), }; - let join_handle = livekit_runtime::spawn(task.run()); + let pipeline_handle = livekit_runtime::spawn(pipeline.run()); self.descriptors.insert( info.pub_handle, - Descriptor::Active { state_tx: state_tx.clone(), join_handle }, + Descriptor::Active { state_tx: state_tx.clone(), pipeline_handle }, ); let inner = LocalTrackInner { frame_tx, state_tx }; @@ -267,11 +267,11 @@ impl Manager { Descriptor::Pending(result_tx) => { _ = result_tx.send(Err(PublishError::Disconnected)) } - Descriptor::Active { state_tx, join_handle } => { + Descriptor::Active { state_tx, pipeline_handle } => { _ = state_tx.send(LocalTrackState::Unpublished { initiator: UnpublishInitiator::Shutdown, }); - join_handle.await; + pipeline_handle.await; } } } @@ -298,7 +298,7 @@ enum Descriptor { /// Active { state_tx: watch::Sender, - join_handle: livekit_runtime::JoinHandle<()>, + pipeline_handle: livekit_runtime::JoinHandle<()>, }, } diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 7a063196f..1fdc7fc6b 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -22,8 +22,8 @@ use crate::{ use std::sync::Arc; use tokio::sync::{mpsc, watch}; -/// Task responsible for publishing frames for an individual data track. -pub(super) struct LocalTrackTask { +/// Pipeline for an individual published data track. +pub(super) struct Pipeline { pub packetizer: Packetizer, pub e2ee_provider: Option>, pub info: Arc, @@ -32,9 +32,10 @@ pub(super) struct LocalTrackTask { pub event_out_tx: mpsc::Sender, } -impl LocalTrackTask { +impl Pipeline { + /// Run the pipeline task, consuming self. pub async fn run(mut self) { - log::debug!("Task started: sid={}", self.info.sid); + log::debug!("Pipeline task started: sid={}", self.info.sid); let mut state = *self.state_rx.borrow(); while state.is_published() { tokio::select! { @@ -52,7 +53,7 @@ impl LocalTrackTask { let event = UnpublishRequestEvent { handle: self.info.pub_handle }; _ = self.event_out_tx.try_send(event.into()); } - log::debug!("Task ended: sid={}", self.info.sid); + log::debug!("Pipeline task ended: sid={}", self.info.sid); } fn publish_frame(&mut self, mut frame: DataTrackFrame) { @@ -62,7 +63,7 @@ impl LocalTrackTask { let encrypted_payload = match e2ee_provider.encrypt(frame.payload) { Ok(payload) => payload, Err(err) => { - log::error!("Failed to encrypt frame: {}", err); + log::error!("{}", err); return; } }; diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 49eba6f09..2a504873a 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{depacketizer::Depacketizer, RemoteDataTrack, RemoteTrackInner}; +use super::{depacketizer::Depacketizer, pipeline::Pipeline, RemoteDataTrack, RemoteTrackInner}; use crate::{ api::{DataTrackFrame, DataTrackInfo, DataTrackSid, InternalError, SubscribeError}, dtp::{Dtp, Handle}, e2ee::DecryptionProvider, - remote::pipeline::RemoteTrackTask, utils::HandleMap, }; use anyhow::{anyhow, Context}; @@ -284,7 +283,7 @@ impl Manager { } else { None }; - let track_task = RemoteTrackTask { + let pipeline = Pipeline { depacketizer: Depacketizer::new(), e2ee_provider, info: descriptor.info.clone(), @@ -294,9 +293,9 @@ impl Manager { frame_tx: frame_tx.clone(), event_out_tx: self.event_out_tx.downgrade(), }; - let join_handle = livekit_runtime::spawn(track_task.run()); + let pipeline_handle = livekit_runtime::spawn(pipeline.run()); - descriptor.state = DescriptorState::Subscribed { packet_tx, frame_tx, join_handle }; + descriptor.state = DescriptorState::Subscribed { packet_tx, frame_tx, pipeline_handle }; self.sub_handles.insert(handle, sid); for result_tx in result_txs { @@ -326,7 +325,7 @@ impl Manager { }; _ = packet_tx .try_send(dtp) - .inspect_err(|err| log::debug!("Cannot send packet to track task: {}", err)); + .inspect_err(|err| log::debug!("Cannot send packet to track pipeline: {}", err)); } /// Performs cleanup before the task ends. @@ -340,7 +339,7 @@ impl Manager { _ = result_tx.send(Err(SubscribeError::Disconnected)); } } - DescriptorState::Subscribed { join_handle, .. } => join_handle.await, + DescriptorState::Subscribed { pipeline_handle, .. } => pipeline_handle.await, } } } @@ -363,7 +362,7 @@ enum DescriptorState { Subscribed { packet_tx: mpsc::Sender, frame_tx: broadcast::Sender, - join_handle: livekit_runtime::JoinHandle<()>, + pipeline_handle: livekit_runtime::JoinHandle<()>, }, } diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index ca042f6ec..f2d087484 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -25,8 +25,8 @@ use crate::{ use std::sync::Arc; use tokio::sync::{broadcast, mpsc, watch}; -/// Task responsible for receiving frames for a subscribed data track. -pub(super) struct RemoteTrackTask { +/// Pipeline for an individual data track with an active subscription. +pub(super) struct Pipeline { pub depacketizer: Depacketizer, pub e2ee_provider: Option>, pub info: Arc, @@ -37,7 +37,8 @@ pub(super) struct RemoteTrackTask { pub event_out_tx: mpsc::WeakSender, } -impl RemoteTrackTask { +impl Pipeline { + /// Run the pipeline task, consuming self. pub async fn run(mut self) { log::debug!("Task started: sid={}", self.info.sid); let mut state = *self.state_rx.borrow(); From 7b09bc617b59e19fd14a6229d285a7618211cd3f Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:08:20 +1100 Subject: [PATCH 173/232] Use weak sender --- livekit-datatrack/src/local/manager.rs | 2 +- livekit-datatrack/src/local/pipeline.rs | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index f2304c5a2..cfbcea7a4 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -233,7 +233,7 @@ impl Manager { info: info.clone(), frame_rx, state_rx, - event_out_tx: self.event_out_tx.clone(), + event_out_tx: self.event_out_tx.downgrade(), }; let pipeline_handle = livekit_runtime::spawn(pipeline.run()); self.descriptors.insert( diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 1fdc7fc6b..7c05cbc72 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -29,7 +29,7 @@ pub(super) struct Pipeline { pub info: Arc, pub state_rx: watch::Receiver, pub frame_rx: mpsc::Receiver, - pub event_out_tx: mpsc::Sender, + pub event_out_tx: mpsc::WeakSender, } impl Pipeline { @@ -51,7 +51,9 @@ impl Pipeline { } if let LocalTrackState::Unpublished { initiator: UnpublishInitiator::Client } = state { let event = UnpublishRequestEvent { handle: self.info.pub_handle }; - _ = self.event_out_tx.try_send(event.into()); + if let Some(event_out_tx) = self.event_out_tx.upgrade() { + _ = event_out_tx.try_send(event.into()); + } } log::debug!("Pipeline task ended: sid={}", self.info.sid); } @@ -91,9 +93,10 @@ impl Pipeline { }; let packets: Vec<_> = packets.into_iter().map(|dtp| dtp.serialize()).collect(); - _ = self - .event_out_tx - .try_send(packets.into()) - .inspect_err(|err| log::debug!("Cannot send packet to transport: {}", err)); + if let Some(event_out_tx) = self.event_out_tx.upgrade() { + _ = event_out_tx + .try_send(packets.into()) + .inspect_err(|err| log::debug!("Cannot send packet to transport: {}", err)); + } } } From 27c57ba300d05f65222067b64933d88304f29306 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:34:37 +1100 Subject: [PATCH 174/232] Move to separate function --- livekit-datatrack/src/local/pipeline.rs | 59 +++++++++++++----------- livekit-datatrack/src/remote/pipeline.rs | 44 ++++++++++-------- 2 files changed, 57 insertions(+), 46 deletions(-) diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 7c05cbc72..1c3e37bdd 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -58,31 +58,8 @@ impl Pipeline { log::debug!("Pipeline task ended: sid={}", self.info.sid); } - fn publish_frame(&mut self, mut frame: DataTrackFrame) { - let mut e2ee: Option = None; - if let Some(e2ee_provider) = &self.e2ee_provider { - debug_assert!(self.info.uses_e2ee); - let encrypted_payload = match e2ee_provider.encrypt(frame.payload) { - Ok(payload) => payload, - Err(err) => { - log::error!("{}", err); - return; - } - }; - e2ee = Some(dtp::E2eeExt { - key_index: encrypted_payload.key_index, - iv: encrypted_payload.iv, - }); - frame.payload = encrypted_payload.payload; - } - - let frame = PacketizerFrame { - payload: frame.payload, - extensions: Extensions { - e2ee, - user_timestamp: frame.user_timestamp.map(|v| UserTimestampExt(v)), - }, - }; + fn publish_frame(&mut self, frame: DataTrackFrame) { + let Some(frame) = self.encrypt_if_needed(frame.into()) else { return }; let packets = match self.packetizer.packetize(frame) { Ok(packets) => packets, @@ -91,7 +68,6 @@ impl Pipeline { return; } }; - let packets: Vec<_> = packets.into_iter().map(|dtp| dtp.serialize()).collect(); if let Some(event_out_tx) = self.event_out_tx.upgrade() { _ = event_out_tx @@ -99,4 +75,35 @@ impl Pipeline { .inspect_err(|err| log::debug!("Cannot send packet to transport: {}", err)); } } + + /// Encrypt the frame's payload if E2EE is enabled for this track. + fn encrypt_if_needed(&self, mut frame: PacketizerFrame) -> Option { + let Some(e2ee_provider) = &self.e2ee_provider else { return frame.into() }; + debug_assert!(self.info.uses_e2ee); + + let encrypted = match e2ee_provider.encrypt(frame.payload) { + Ok(payload) => payload, + Err(err) => { + log::error!("{}", err); + return None; + } + }; + + frame.payload = encrypted.payload; + frame.extensions.e2ee = + dtp::E2eeExt { key_index: encrypted.key_index, iv: encrypted.iv }.into(); + frame.into() + } +} + +impl From for PacketizerFrame { + fn from(frame: DataTrackFrame) -> Self { + Self { + payload: frame.payload, + extensions: Extensions { + user_timestamp: frame.user_timestamp.map(|v| UserTimestampExt(v)), + e2ee: None, + }, + } + } } diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index f2d087484..dd77c6a15 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -59,28 +59,32 @@ impl Pipeline { } fn receive_packet(&mut self, dtp: Dtp) { - let Some(mut frame) = self.depacketizer.push(dtp) else { return }; - if let Some(decryption) = &self.e2ee_provider { - debug_assert!(self.info.uses_e2ee); - - let Some(e2ee) = frame.extensions.e2ee else { - log::error!("Missing E2EE meta"); - return; - }; - let encrypted_payload = - EncryptedPayload { payload: frame.payload, iv: e2ee.iv, key_index: e2ee.key_index }; - let decrypted_payload = - match decryption.decrypt(encrypted_payload, &self.publisher_identity) { - Ok(decrypted_payload) => decrypted_payload, - Err(err) => { - log::error!("{}", err); - return; - } - }; - frame.payload = decrypted_payload; - } + let Some(frame) = self.depacketizer.push(dtp) else { return }; + let Some(frame) = self.decrypt_if_needed(frame) else { return }; _ = self.frame_tx.send(frame.into()); } + + /// Decrypt the frame's payload if E2EE is enabled for this track. + fn decrypt_if_needed(&self, mut frame: DepacketizerFrame) -> Option { + let Some(decryption) = &self.e2ee_provider else { return frame.into() }; + debug_assert!(self.info.uses_e2ee); + + let Some(e2ee) = frame.extensions.e2ee else { + log::error!("Missing E2EE meta"); + return None; + }; + + let encrypted = + EncryptedPayload { payload: frame.payload, iv: e2ee.iv, key_index: e2ee.key_index }; + frame.payload = match decryption.decrypt(encrypted, &self.publisher_identity) { + Ok(decrypted) => decrypted, + Err(err) => { + log::error!("{}", err); + return None; + } + }; + frame.into() + } } impl From for DataTrackFrame { From e71c6987869ba43c654b3aa089572d92be1815d7 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:57:31 +1100 Subject: [PATCH 175/232] Private fields in pipeline --- livekit-datatrack/src/local/manager.rs | 8 +++--- livekit-datatrack/src/local/pipeline.rs | 33 ++++++++++++++++++++--- livekit-datatrack/src/remote/manager.rs | 7 ++--- livekit-datatrack/src/remote/pipeline.rs | 34 +++++++++++++++++++++--- 4 files changed, 67 insertions(+), 15 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index cfbcea7a4..0733897fd 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{packetizer::Packetizer, pipeline::Pipeline, LocalTrackInner}; +use super::{pipeline::{Pipeline, PipelineOptions}, LocalTrackInner}; use crate::{ api::{DataTrackInfo, DataTrackOptions, InternalError, PublishError}, dtp::{self, Handle}, @@ -226,16 +226,16 @@ impl Manager { } else { None }; - let pipeline = Pipeline { - // TODO: handle cancellation - packetizer: Packetizer::new(info.pub_handle, Self::TRANSPORT_MTU), + let options = PipelineOptions { e2ee_provider, info: info.clone(), frame_rx, state_rx, event_out_tx: self.event_out_tx.downgrade(), }; + let pipeline = Pipeline::new(options); let pipeline_handle = livekit_runtime::spawn(pipeline.run()); + self.descriptors.insert( info.pub_handle, Descriptor::Active { state_tx: state_tx.clone(), pipeline_handle }, diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 1c3e37bdd..1aae22155 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -22,9 +22,8 @@ use crate::{ use std::sync::Arc; use tokio::sync::{mpsc, watch}; -/// Pipeline for an individual published data track. -pub(super) struct Pipeline { - pub packetizer: Packetizer, +/// Options for creating a [`Pipeline`]. +pub(super) struct PipelineOptions { pub e2ee_provider: Option>, pub info: Arc, pub state_rx: watch::Receiver, @@ -32,7 +31,31 @@ pub(super) struct Pipeline { pub event_out_tx: mpsc::WeakSender, } +/// Pipeline for an individual published data track. +pub(super) struct Pipeline { + packetizer: Packetizer, + e2ee_provider: Option>, + info: Arc, + state_rx: watch::Receiver, + frame_rx: mpsc::Receiver, + event_out_tx: mpsc::WeakSender, +} + impl Pipeline { + /// Creates a new pipeline with the given options. + pub fn new(options: PipelineOptions) -> Self { + debug_assert_eq!(options.info.uses_e2ee, options.e2ee_provider.is_some()); + let packetizer = Packetizer::new(options.info.pub_handle, Self::TRANSPORT_MTU); + Self { + packetizer, + e2ee_provider: options.e2ee_provider, + info: options.info, + state_rx: options.state_rx, + frame_rx: options.frame_rx, + event_out_tx: options.event_out_tx, + } + } + /// Run the pipeline task, consuming self. pub async fn run(mut self) { log::debug!("Pipeline task started: sid={}", self.info.sid); @@ -79,7 +102,6 @@ impl Pipeline { /// Encrypt the frame's payload if E2EE is enabled for this track. fn encrypt_if_needed(&self, mut frame: PacketizerFrame) -> Option { let Some(e2ee_provider) = &self.e2ee_provider else { return frame.into() }; - debug_assert!(self.info.uses_e2ee); let encrypted = match e2ee_provider.encrypt(frame.payload) { Ok(payload) => payload, @@ -94,6 +116,9 @@ impl Pipeline { dtp::E2eeExt { key_index: encrypted.key_index, iv: encrypted.iv }.into(); frame.into() } + + /// Maximum transmission unit (MTU) of the transport. + const TRANSPORT_MTU: usize = 16_000; } impl From for PacketizerFrame { diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 2a504873a..fe42b6d50 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{depacketizer::Depacketizer, pipeline::Pipeline, RemoteDataTrack, RemoteTrackInner}; +use super::{pipeline::{Pipeline, PipelineOptions}, RemoteDataTrack, RemoteTrackInner}; use crate::{ api::{DataTrackFrame, DataTrackInfo, DataTrackSid, InternalError, SubscribeError}, dtp::{Dtp, Handle}, @@ -283,8 +283,8 @@ impl Manager { } else { None }; - let pipeline = Pipeline { - depacketizer: Depacketizer::new(), + + let options = PipelineOptions { e2ee_provider, info: descriptor.info.clone(), publisher_identity: descriptor.publisher_identity.clone(), @@ -293,6 +293,7 @@ impl Manager { frame_tx: frame_tx.clone(), event_out_tx: self.event_out_tx.downgrade(), }; + let pipeline = Pipeline::new(options); let pipeline_handle = livekit_runtime::spawn(pipeline.run()); descriptor.state = DescriptorState::Subscribed { packet_tx, frame_tx, pipeline_handle }; diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index dd77c6a15..b01f61076 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -25,9 +25,8 @@ use crate::{ use std::sync::Arc; use tokio::sync::{broadcast, mpsc, watch}; -/// Pipeline for an individual data track with an active subscription. -pub(super) struct Pipeline { - pub depacketizer: Depacketizer, +/// Options for creating a [`Pipeline`]. +pub(super) struct PipelineOptions { pub e2ee_provider: Option>, pub info: Arc, pub publisher_identity: Arc, @@ -37,7 +36,35 @@ pub(super) struct Pipeline { pub event_out_tx: mpsc::WeakSender, } +/// Pipeline for an individual data track with an active subscription. +pub(super) struct Pipeline { + depacketizer: Depacketizer, + e2ee_provider: Option>, + info: Arc, + publisher_identity: Arc, + state_rx: watch::Receiver, + packet_rx: mpsc::Receiver, + frame_tx: broadcast::Sender, + event_out_tx: mpsc::WeakSender, +} + impl Pipeline { + /// Creates a new pipeline with the given options. + pub fn new(options: PipelineOptions) -> Self { + debug_assert_eq!(options.info.uses_e2ee, options.e2ee_provider.is_some()); + let depacketizer = Depacketizer::new(); + Self { + depacketizer, + e2ee_provider: options.e2ee_provider, + info: options.info, + publisher_identity: options.publisher_identity, + state_rx: options.state_rx, + packet_rx: options.packet_rx, + frame_tx: options.frame_tx, + event_out_tx: options.event_out_tx, + } + } + /// Run the pipeline task, consuming self. pub async fn run(mut self) { log::debug!("Task started: sid={}", self.info.sid); @@ -67,7 +94,6 @@ impl Pipeline { /// Decrypt the frame's payload if E2EE is enabled for this track. fn decrypt_if_needed(&self, mut frame: DepacketizerFrame) -> Option { let Some(decryption) = &self.e2ee_provider else { return frame.into() }; - debug_assert!(self.info.uses_e2ee); let Some(e2ee) = frame.extensions.e2ee else { log::error!("Missing E2EE meta"); From b270f2b8d2bc248a65af0afe15fa5f9714306da5 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:26:21 +1100 Subject: [PATCH 176/232] Local pipeline unit testing --- livekit-datatrack/src/local/pipeline.rs | 52 +++++++++++++++++++++++++ livekit-datatrack/src/track.rs | 3 +- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 1aae22155..c43b7c23f 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -132,3 +132,55 @@ impl From for PacketizerFrame { } } } + +#[cfg(test)] +mod tests { + use super::*; + use bytes::Bytes; + use fake::{Fake, Faker}; + + fn make_pipeline( + ) -> (watch::Sender, mpsc::Sender, mpsc::Receiver) + { + let (state_tx, state_rx) = watch::channel(LocalTrackState::Published); + let (frame_tx, frame_rx) = mpsc::channel(32); + let (event_out_tx, event_out_rx) = mpsc::channel(32); + + let mut info: DataTrackInfo = Faker.fake(); + info.uses_e2ee = false; + + let options = PipelineOptions { + e2ee_provider: None, + info: info.into(), + state_rx, + frame_rx, + event_out_tx: event_out_tx.downgrade(), + }; + let pipeline = Pipeline::new(options); + livekit_runtime::spawn(pipeline.run()); + + (state_tx, frame_tx, event_out_rx) + } + + #[tokio::test] + async fn test_publish_frame() { + let (_, frame_tx, mut event_out_rx) = make_pipeline(); + + let frame = DataTrackFrame { + payload: Bytes::from(vec![0xFA; 256]), + user_timestamp: Faker.fake() + }; + frame_tx.send(frame).await.unwrap(); + + while let Some(out_event) = event_out_rx.recv().await { + let OutputEvent::PacketsAvailable(packets) = out_event else { + panic!("Unexpected event") + }; + let Some(packet) = packets.first() else { + panic!("Expected one packet") + }; + assert!(!packet.is_empty()); + break; + } + } +} diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index 91622c6df..b43125ed3 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -41,6 +41,7 @@ impl DataTrack { /// Information about a published data track. #[derive(Debug, Clone)] +#[cfg_attr(test, derive(fake::Dummy))] pub struct DataTrackInfo { pub(crate) sid: DataTrackSid, pub(crate) pub_handle: Handle, @@ -120,4 +121,4 @@ impl fake::Dummy for DataTrackSid { .collect(); Self::try_from(format!("{}{}", Self::PREFIX, random_id)).unwrap() } -} +} \ No newline at end of file From e7d0982943588b25b7a9fb956005cb9ee070325d Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:15:45 +1100 Subject: [PATCH 177/232] Remove unused --- livekit-datatrack/src/local/manager.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 0733897fd..3841f2764 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -279,9 +279,6 @@ impl Manager { /// How long to wait for an SFU response for a track publication before timeout. const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); - - /// MTU of the transport - const TRANSPORT_MTU: usize = 16_000; } #[derive(Debug)] From 2d53aa70993e03393f4ad5f7c2ba1d729189118f Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:25:32 +1100 Subject: [PATCH 178/232] Rename dtp to packet --- livekit-datatrack/src/lib.rs | 2 +- livekit-datatrack/src/local/manager.rs | 10 +-- livekit-datatrack/src/local/packetizer.rs | 8 +- livekit-datatrack/src/local/pipeline.rs | 6 +- livekit-datatrack/src/local/proto.rs | 2 +- .../src/{dtp => packet}/deserialize.rs | 52 ++++++------ .../src/{dtp => packet}/extension.rs | 0 .../src/{dtp => packet}/handle.rs | 0 livekit-datatrack/src/{dtp => packet}/mod.rs | 12 ++- .../src/{dtp => packet}/serialize.rs | 18 ++-- livekit-datatrack/src/{dtp => packet}/time.rs | 0 livekit-datatrack/src/remote/depacketizer.rs | 82 +++++++++---------- livekit-datatrack/src/remote/manager.rs | 16 ++-- livekit-datatrack/src/remote/pipeline.rs | 14 ++-- livekit-datatrack/src/remote/proto.rs | 2 +- livekit-datatrack/src/track.rs | 2 +- livekit-datatrack/src/utils/handle_map.rs | 2 +- 17 files changed, 113 insertions(+), 115 deletions(-) rename livekit-datatrack/src/{dtp => packet}/deserialize.rs (82%) rename livekit-datatrack/src/{dtp => packet}/extension.rs (100%) rename livekit-datatrack/src/{dtp => packet}/handle.rs (100%) rename livekit-datatrack/src/{dtp => packet}/mod.rs (95%) rename livekit-datatrack/src/{dtp => packet}/serialize.rs (93%) rename livekit-datatrack/src/{dtp => packet}/time.rs (100%) diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index 2a159e11f..d13791ce9 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -28,7 +28,7 @@ mod frame; mod e2ee; /// Data track packet (DTP) format. -mod dtp; +mod packet; /// Internal utilities. mod utils; diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 3841f2764..2891dcd3e 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -15,7 +15,7 @@ use super::{pipeline::{Pipeline, PipelineOptions}, LocalTrackInner}; use crate::{ api::{DataTrackInfo, DataTrackOptions, InternalError, PublishError}, - dtp::{self, Handle}, + packet::{self, Handle}, e2ee::EncryptionProvider, local::LocalDataTrack, }; @@ -115,7 +115,7 @@ pub struct Manager { event_in_tx: mpsc::WeakSender, event_in_rx: mpsc::Receiver, event_out_tx: mpsc::Sender, - handle_allocator: dtp::HandleAllocator, + handle_allocator: packet::HandleAllocator, descriptors: HashMap, } @@ -138,7 +138,7 @@ impl Manager { event_in_tx: event_in_tx.downgrade(), event_in_rx, event_out_tx, - handle_allocator: dtp::HandleAllocator::default(), + handle_allocator: packet::HandleAllocator::default(), descriptors: HashMap::new(), }; @@ -348,7 +348,7 @@ impl ManagerInput { #[cfg(test)] mod tests { use super::*; - use crate::{api::DataTrackSid, dtp::Dtp}; + use crate::{api::DataTrackSid, packet::Packet}; use fake::{faker::lorem::en::Word, Fake, Faker}; use futures_util::StreamExt; use livekit_runtime::sleep; @@ -399,7 +399,7 @@ mod tests { } OutputEvent::PacketsAvailable(packets) => { let packet = packets.into_iter().nth(0).unwrap(); - let payload = Dtp::deserialize(packet).unwrap().payload; + let payload = Packet::deserialize(packet).unwrap().payload; assert_eq!(payload.len(), payload_size); packets_sent += 1; } diff --git a/livekit-datatrack/src/local/packetizer.rs b/livekit-datatrack/src/local/packetizer.rs index 873e1f701..f36728ff9 100644 --- a/livekit-datatrack/src/local/packetizer.rs +++ b/livekit-datatrack/src/local/packetizer.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::{ - dtp::{Clock, Dtp, Extensions, FrameMarker, Handle, Header, Timestamp}, + packet::{Clock, Packet, Extensions, FrameMarker, Handle, Header, Timestamp}, utils::{BytesChunkExt, Counter}, }; use bytes::Bytes; @@ -54,7 +54,7 @@ impl Packetizer { } /// Packetizes a frame into one or more packets. - pub fn packetize(&mut self, frame: PacketizerFrame) -> Result, PacketizerError> { + pub fn packetize(&mut self, frame: PacketizerFrame) -> Result, PacketizerError> { // TODO: consider using default let header = Header { marker: FrameMarker::Inter, @@ -74,7 +74,7 @@ impl Packetizer { let packets = packet_payloads .into_iter() .enumerate() - .map(|(index, payload)| Dtp { + .map(|(index, payload)| Packet { header: Header { marker: Self::frame_marker(index, packet_count), sequence: self.sequence.get_then_increment(), @@ -102,7 +102,7 @@ impl Packetizer { #[cfg(test)] mod tests { use super::*; - use crate::dtp::Handle; + use crate::packet::Handle; use fake::{Fake, Faker}; use test_case::test_case; diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index c43b7c23f..0ebef42ce 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -15,7 +15,7 @@ use super::packetizer::{Packetizer, PacketizerFrame}; use crate::{ api::{DataTrackFrame, DataTrackInfo}, - dtp::{self, Extensions, UserTimestampExt}, + packet::{self, Extensions, UserTimestampExt}, e2ee::EncryptionProvider, local::manager::{LocalTrackState, OutputEvent, UnpublishInitiator, UnpublishRequestEvent}, }; @@ -91,7 +91,7 @@ impl Pipeline { return; } }; - let packets: Vec<_> = packets.into_iter().map(|dtp| dtp.serialize()).collect(); + let packets: Vec<_> = packets.into_iter().map(|packet| packet.serialize()).collect(); if let Some(event_out_tx) = self.event_out_tx.upgrade() { _ = event_out_tx .try_send(packets.into()) @@ -113,7 +113,7 @@ impl Pipeline { frame.payload = encrypted.payload; frame.extensions.e2ee = - dtp::E2eeExt { key_index: encrypted.key_index, iv: encrypted.iv }.into(); + packet::E2eeExt { key_index: encrypted.key_index, iv: encrypted.iv }.into(); frame.into() } diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index 9cfe641f9..143e96dc3 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -15,7 +15,7 @@ use super::manager::{PublishRequestEvent, UnpublishEvent, UnpublishRequestEvent}; use crate::{ api::{DataTrackInfo, DataTrackSid, InternalError, PublishError}, - dtp::Handle, + packet::Handle, local::manager::PublishResultEvent, }; use anyhow::{anyhow, Context}; diff --git a/livekit-datatrack/src/dtp/deserialize.rs b/livekit-datatrack/src/packet/deserialize.rs similarity index 82% rename from livekit-datatrack/src/dtp/deserialize.rs rename to livekit-datatrack/src/packet/deserialize.rs index 2e63e1511..cd78db3ad 100644 --- a/livekit-datatrack/src/dtp/deserialize.rs +++ b/livekit-datatrack/src/packet/deserialize.rs @@ -13,7 +13,7 @@ // limitations under the License. use super::{ - consts::*, Dtp, E2eeExt, ExtensionTag, Extensions, FrameMarker, Handle, HandleError, Header, + consts::*, Packet, E2eeExt, ExtensionTag, Extensions, FrameMarker, Handle, HandleError, Header, Timestamp, UserTimestampExt, }; use bytes::{Buf, Bytes}; @@ -40,7 +40,7 @@ pub enum DeserializeError { MalformedExt(ExtensionTag), } -impl Dtp { +impl Packet { pub fn deserialize(mut raw: Bytes) -> Result { let header = Header::deserialize(&mut raw)?; let payload_len = raw.remaining(); @@ -152,8 +152,8 @@ mod tests { let mut raw = valid_packet(); raw.truncate(11); - let dtp = Dtp::deserialize(raw.freeze()); - assert!(matches!(dtp, Err(DeserializeError::TooShort))); + let packet = Packet::deserialize(raw.freeze()); + assert!(matches!(packet, Err(DeserializeError::TooShort))); } #[test] @@ -162,8 +162,8 @@ mod tests { raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag // Should have ext word indicator here - let dtp = Dtp::deserialize(raw.freeze()); - assert!(matches!(dtp, Err(DeserializeError::MissingExtWords))); + let packet = Packet::deserialize(raw.freeze()); + assert!(matches!(packet, Err(DeserializeError::MissingExtWords))); } #[test] @@ -172,8 +172,8 @@ mod tests { raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag raw.put_u16(1); // One extension word - let dtp = Dtp::deserialize(raw.freeze()); - assert!(matches!(dtp, Err(DeserializeError::HeaderOverrun))); + let packet = Packet::deserialize(raw.freeze()); + assert!(matches!(packet, Err(DeserializeError::HeaderOverrun))); } #[test] @@ -181,8 +181,8 @@ mod tests { let mut raw = valid_packet(); raw[0] = 0x20; // Version 1 (not supported yet) - let dtp = Dtp::deserialize(raw.freeze()); - assert!(matches!(dtp, Err(DeserializeError::UnsupportedVersion(1)))); + let packet = Packet::deserialize(raw.freeze()); + assert!(matches!(packet, Err(DeserializeError::UnsupportedVersion(1)))); } #[test] @@ -195,14 +195,14 @@ mod tests { raw.put_slice(&[0x44, 0x11]); // Frame number raw.put_slice(&[0x44, 0x22, 0x11, 0x88]); // Timestamp - let dtp = Dtp::deserialize(raw.freeze()).unwrap(); - assert_eq!(dtp.header.marker, FrameMarker::Final); - assert_eq!(dtp.header.track_handle, 0x8811u32.try_into().unwrap()); - assert_eq!(dtp.header.sequence, 0x4422); - assert_eq!(dtp.header.frame_number, 0x4411); - assert_eq!(dtp.header.timestamp, Timestamp::from_ticks(0x44221188)); - assert_eq!(dtp.header.extensions.user_timestamp, None); - assert_eq!(dtp.header.extensions.e2ee, None); + let packet = Packet::deserialize(raw.freeze()).unwrap(); + assert_eq!(packet.header.marker, FrameMarker::Final); + assert_eq!(packet.header.track_handle, 0x8811u32.try_into().unwrap()); + assert_eq!(packet.header.sequence, 0x4422); + assert_eq!(packet.header.frame_number, 0x4411); + assert_eq!(packet.header.timestamp, Timestamp::from_ticks(0x44221188)); + assert_eq!(packet.header.extensions.user_timestamp, None); + assert_eq!(packet.header.extensions.e2ee, None); } #[test_matrix([0, 1, 24])] @@ -213,8 +213,8 @@ mod tests { raw.put_u16(ext_words as u16); // 4 extension word raw.put_bytes(0, ext_words * 4); // Padding - let dtp = Dtp::deserialize(raw.freeze()).unwrap(); - assert_eq!(dtp.payload.len(), 0); + let packet = Packet::deserialize(raw.freeze()).unwrap(); + assert_eq!(packet.payload.len(), 0); } #[test] @@ -229,8 +229,8 @@ mod tests { raw.put_bytes(0x3C, 12); // IV raw.put_bytes(0, 3); // Padding - let dtp = Dtp::deserialize(raw.freeze()).unwrap(); - let e2ee = dtp.header.extensions.e2ee.unwrap(); + let packet = Packet::deserialize(raw.freeze()).unwrap(); + let e2ee = packet.header.extensions.e2ee.unwrap(); assert_eq!(e2ee.key_index, 0xFA); assert_eq!(e2ee.iv, [0x3C; 12]); } @@ -245,9 +245,9 @@ mod tests { raw.put_u16(7); raw.put_slice(&[0x44, 0x11, 0x22, 0x11, 0x11, 0x11, 0x88, 0x11]); // User timestamp - let dtp = Dtp::deserialize(raw.freeze()).unwrap(); + let packet = Packet::deserialize(raw.freeze()).unwrap(); assert_eq!( - dtp.header.extensions.user_timestamp, + packet.header.extensions.user_timestamp, UserTimestampExt(0x4411221111118811).into() ); } @@ -260,7 +260,7 @@ mod tests { raw.put_u16(8); // ID 8 (unknown) raw.put_bytes(0, 6); - Dtp::deserialize(raw.freeze()).expect("Should skip unknown extension"); + Packet::deserialize(raw.freeze()).expect("Should skip unknown extension"); } #[test] @@ -270,6 +270,6 @@ mod tests { raw.put_u16(1); // Extension words raw.put_bytes(0, 3); // Padding, missing one byte - assert!(Dtp::deserialize(raw.freeze()).is_err()); + assert!(Packet::deserialize(raw.freeze()).is_err()); } } diff --git a/livekit-datatrack/src/dtp/extension.rs b/livekit-datatrack/src/packet/extension.rs similarity index 100% rename from livekit-datatrack/src/dtp/extension.rs rename to livekit-datatrack/src/packet/extension.rs diff --git a/livekit-datatrack/src/dtp/handle.rs b/livekit-datatrack/src/packet/handle.rs similarity index 100% rename from livekit-datatrack/src/dtp/handle.rs rename to livekit-datatrack/src/packet/handle.rs diff --git a/livekit-datatrack/src/dtp/mod.rs b/livekit-datatrack/src/packet/mod.rs similarity index 95% rename from livekit-datatrack/src/dtp/mod.rs rename to livekit-datatrack/src/packet/mod.rs index 895983587..c0bcd76bd 100644 --- a/livekit-datatrack/src/dtp/mod.rs +++ b/livekit-datatrack/src/packet/mod.rs @@ -21,14 +21,12 @@ mod handle; mod serialize; mod time; -pub use deserialize::*; pub use extension::*; pub use handle::*; -pub use serialize::*; pub use time::*; #[derive(Clone)] -pub struct Dtp { +pub struct Packet { pub header: Header, pub payload: Bytes, } @@ -48,19 +46,19 @@ pub struct Header { #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(fake::Dummy))] pub enum FrameMarker { + /// Packet is the first in a frame. + Start, /// Packet is within a frame. Inter, /// Packet is the last in a frame. Final, - /// Packet is the first in a frame. - Start, /// Packet is the only one in a frame. Single, } -impl fmt::Debug for Dtp { +impl fmt::Debug for Packet { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Dtp") + f.debug_struct("Packet") .field("header", &self.header) .field("payload_len", &self.payload.len()) .finish() diff --git a/livekit-datatrack/src/dtp/serialize.rs b/livekit-datatrack/src/packet/serialize.rs similarity index 93% rename from livekit-datatrack/src/dtp/serialize.rs rename to livekit-datatrack/src/packet/serialize.rs index 39f211699..0149ed206 100644 --- a/livekit-datatrack/src/dtp/serialize.rs +++ b/livekit-datatrack/src/packet/serialize.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{consts::*, Dtp, E2eeExt, Extensions, FrameMarker, Header, UserTimestampExt}; +use super::{consts::*, Packet, E2eeExt, Extensions, FrameMarker, Header, UserTimestampExt}; use bytes::{BufMut, Bytes, BytesMut}; use thiserror::Error; @@ -25,7 +25,7 @@ pub enum SerializeError { TooSmallForPayload, } -impl Dtp { +impl Packet { /// Length of the serialized packet in bytes. pub fn serialized_len(&self) -> usize { self.header.serialized_len() + self.payload.len() @@ -164,12 +164,12 @@ impl UserTimestampExt { #[cfg(test)] mod tests { - use crate::dtp::{Dtp, E2eeExt, Extensions, FrameMarker, Header, Timestamp, UserTimestampExt}; + use crate::packet::{Packet, E2eeExt, Extensions, FrameMarker, Header, Timestamp, UserTimestampExt}; use bytes::Buf; /// Constructed packet to use in tests. - fn packet() -> Dtp { - Dtp { + fn packet() -> Packet { + Packet { header: Header { marker: FrameMarker::Final, track_handle: 0x8811u32.try_into().unwrap(), @@ -195,10 +195,10 @@ mod tests { #[test] fn test_serialized_length() { - let dtp = packet(); - assert_eq!(dtp.serialized_len(), 1070); - assert_eq!(dtp.header.serialized_len(), 46); - assert_eq!(dtp.header.extensions.serialized_len(), 29); + let packet = packet(); + assert_eq!(packet.serialized_len(), 1070); + assert_eq!(packet.header.serialized_len(), 46); + assert_eq!(packet.header.extensions.serialized_len(), 29); } #[test] diff --git a/livekit-datatrack/src/dtp/time.rs b/livekit-datatrack/src/packet/time.rs similarity index 100% rename from livekit-datatrack/src/dtp/time.rs rename to livekit-datatrack/src/packet/time.rs diff --git a/livekit-datatrack/src/remote/depacketizer.rs b/livekit-datatrack/src/remote/depacketizer.rs index 3b134dd8d..c3261be31 100644 --- a/livekit-datatrack/src/remote/depacketizer.rs +++ b/livekit-datatrack/src/remote/depacketizer.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::dtp::{Dtp, Extensions, FrameMarker}; +use crate::packet::{Packet, Extensions, FrameMarker}; use bytes::{Bytes, BytesMut}; use std::collections::BTreeMap; @@ -39,65 +39,65 @@ impl Depacketizer { } /// Push a packet into the depacketizer, returning a complete frame if one is available. - pub fn push(&mut self, dtp: Dtp) -> Option { - match dtp.header.marker { - FrameMarker::Single => self.frame_from_single(dtp).into(), + pub fn push(&mut self, packet: Packet) -> Option { + match packet.header.marker { + FrameMarker::Single => self.frame_from_single(packet).into(), FrameMarker::Start => { - self.begin_partial(dtp); + self.begin_partial(packet); None } FrameMarker::Inter => { - self.push_to_partial(dtp); + self.push_to_partial(packet); None } FrameMarker::Final => { - self.push_to_partial(dtp); + self.push_to_partial(packet); self.finalize_partial() } } } - fn frame_from_single(&mut self, dtp: Dtp) -> DepacketizerFrame { - debug_assert!(dtp.header.marker == FrameMarker::Single); + fn frame_from_single(&mut self, packet: Packet) -> DepacketizerFrame { + debug_assert!(packet.header.marker == FrameMarker::Single); if self.partial.is_some() { log::trace!("Drop: interrupted"); self.partial = None; } - DepacketizerFrame { payload: dtp.payload, extensions: dtp.header.extensions } + DepacketizerFrame { payload: packet.payload, extensions: packet.header.extensions } } /// Begin assembling a new packet. - fn begin_partial(&mut self, dtp: Dtp) { - debug_assert!(dtp.header.marker == FrameMarker::Start); + fn begin_partial(&mut self, packet: Packet) { + debug_assert!(packet.header.marker == FrameMarker::Start); if self.partial.is_some() { log::trace!("Drop: interrupted"); self.partial = None; } - let start_sequence = dtp.header.sequence; - let payload_len = dtp.payload.len(); + let start_sequence = packet.header.sequence; + let payload_len = packet.payload.len(); let partial = PartialFrame { - frame_number: dtp.header.frame_number, + frame_number: packet.header.frame_number, start_sequence, end_sequence: None, - extensions: dtp.header.extensions, - payloads: BTreeMap::from([(start_sequence, dtp.payload)]), + extensions: packet.header.extensions, + payloads: BTreeMap::from([(start_sequence, packet.payload)]), payload_len, }; self.partial = partial.into(); } /// Push to the existing partial frame. - fn push_to_partial(&mut self, dtp: Dtp) { - debug_assert!(matches!(dtp.header.marker, FrameMarker::Inter | FrameMarker::Final)); + fn push_to_partial(&mut self, packet: Packet) { + debug_assert!(matches!(packet.header.marker, FrameMarker::Inter | FrameMarker::Final)); let Some(mut partial) = self.partial.take() else { log::trace!("Drop: unknown frame"); return; }; - if dtp.header.frame_number != partial.frame_number { + if packet.header.frame_number != partial.frame_number { log::trace!("Drop: interrupted"); return; } @@ -106,11 +106,11 @@ impl Depacketizer { return; } - partial.payload_len += dtp.payload.len(); - partial.payloads.insert(dtp.header.sequence, dtp.payload); + partial.payload_len += packet.payload.len(); + partial.payloads.insert(packet.header.sequence, packet.payload); - if dtp.header.marker == FrameMarker::Final { - partial.end_sequence = dtp.header.sequence.into(); + if packet.header.marker == FrameMarker::Final { + partial.end_sequence = packet.header.sequence.into(); } self.partial = Some(partial); @@ -173,12 +173,12 @@ mod tests { fn test_single_packet() { let mut depacketizer = Depacketizer::new(); - let mut dtp: Dtp = Faker.fake(); - dtp.header.marker = FrameMarker::Single; + let mut packet: Packet = Faker.fake(); + packet.header.marker = FrameMarker::Single; - let frame = depacketizer.push(dtp.clone()).unwrap(); - assert_eq!(frame.payload, dtp.payload); - assert_eq!(frame.extensions, dtp.header.extensions); + let frame = depacketizer.push(packet.clone()).unwrap(); + assert_eq!(frame.payload, packet.payload); + assert_eq!(frame.extensions, packet.header.extensions); } #[test_case(0)] @@ -187,26 +187,26 @@ mod tests { fn test_multi_packet(inter_packets: usize) { let mut depacketizer = Depacketizer::new(); - let mut dtp: Dtp = Faker.fake(); - dtp.header.marker = FrameMarker::Start; + let mut packet: Packet = Faker.fake(); + packet.header.marker = FrameMarker::Start; - assert!(depacketizer.push(dtp.clone()).is_none()); + assert!(depacketizer.push(packet.clone()).is_none()); for _ in 0..inter_packets { - dtp.header.marker = FrameMarker::Inter; - dtp.header.sequence += 1; - assert!(depacketizer.push(dtp.clone()).is_none()); + packet.header.marker = FrameMarker::Inter; + packet.header.sequence += 1; + assert!(depacketizer.push(packet.clone()).is_none()); } - dtp.header.marker = FrameMarker::Final; - dtp.header.sequence += 1; + packet.header.marker = FrameMarker::Final; + packet.header.sequence += 1; - let frame = depacketizer.push(dtp.clone()).unwrap(); - assert_eq!(frame.extensions, dtp.header.extensions); - assert_eq!(frame.payload.len(), dtp.payload.len() * (inter_packets + 2)); + let frame = depacketizer.push(packet.clone()).unwrap(); + assert_eq!(frame.extensions, packet.header.extensions); + assert_eq!(frame.payload.len(), packet.payload.len() * (inter_packets + 2)); } - impl fake::Dummy for Dtp { + impl fake::Dummy for Packet { fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { let payload_len = rng.random_range(0..=1500); let payload = (0..payload_len).map(|_| rng.random()).collect::(); diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index fe42b6d50..622d1d5f7 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -15,7 +15,7 @@ use super::{pipeline::{Pipeline, PipelineOptions}, RemoteDataTrack, RemoteTrackInner}; use crate::{ api::{DataTrackFrame, DataTrackInfo, DataTrackSid, InternalError, SubscribeError}, - dtp::{Dtp, Handle}, + packet::{Packet, Handle}, e2ee::DecryptionProvider, utils::HandleMap, }; @@ -305,15 +305,15 @@ impl Manager { } fn handle_packet_received(&mut self, bytes: Bytes) { - let dtp = match Dtp::deserialize(bytes) { - Ok(dtp) => dtp, + let packet = match Packet::deserialize(bytes) { + Ok(packet) => packet, Err(err) => { - log::error!("Failed to deserialize DTP: {}", err); + log::error!("Failed to deserialize packet: {}", err); return; } }; - let Some(sid) = self.sub_handles.get_sid(dtp.header.track_handle) else { - log::warn!("Unknown subscriber handle {}", dtp.header.track_handle); + let Some(sid) = self.sub_handles.get_sid(packet.header.track_handle) else { + log::warn!("Unknown subscriber handle {}", packet.header.track_handle); return; }; let Some(descriptor) = self.descriptors.get(sid) else { @@ -325,7 +325,7 @@ impl Manager { return; }; _ = packet_tx - .try_send(dtp) + .try_send(packet) .inspect_err(|err| log::debug!("Cannot send packet to track pipeline: {}", err)); } @@ -361,7 +361,7 @@ enum DescriptorState { result_txs: Vec>, }, Subscribed { - packet_tx: mpsc::Sender, + packet_tx: mpsc::Sender, frame_tx: broadcast::Sender, pipeline_handle: livekit_runtime::JoinHandle<()>, }, diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index b01f61076..4a108388f 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -18,7 +18,7 @@ use super::{ }; use crate::{ api::{DataTrackFrame, DataTrackInfo}, - dtp::Dtp, + packet::Packet, e2ee::{DecryptionProvider, EncryptedPayload}, remote::depacketizer::DepacketizerFrame, }; @@ -31,7 +31,7 @@ pub(super) struct PipelineOptions { pub info: Arc, pub publisher_identity: Arc, pub state_rx: watch::Receiver, - pub packet_rx: mpsc::Receiver, + pub packet_rx: mpsc::Receiver, pub frame_tx: broadcast::Sender, pub event_out_tx: mpsc::WeakSender, } @@ -43,7 +43,7 @@ pub(super) struct Pipeline { info: Arc, publisher_identity: Arc, state_rx: watch::Receiver, - packet_rx: mpsc::Receiver, + packet_rx: mpsc::Receiver, frame_tx: broadcast::Sender, event_out_tx: mpsc::WeakSender, } @@ -75,8 +75,8 @@ impl Pipeline { _ = self.state_rx.changed() => { state = *self.state_rx.borrow(); }, - Some(dtp) = self.packet_rx.recv() => { - self.receive_packet(dtp); + Some(packet) = self.packet_rx.recv() => { + self.receive_packet(packet); }, else => break } @@ -85,8 +85,8 @@ impl Pipeline { // TODO: send unsubscribe if needed } - fn receive_packet(&mut self, dtp: Dtp) { - let Some(frame) = self.depacketizer.push(dtp) else { return }; + fn receive_packet(&mut self, packet: Packet) { + let Some(frame) = self.depacketizer.push(packet) else { return }; let Some(frame) = self.decrypt_if_needed(frame) else { return }; _ = self.frame_tx.send(frame.into()); } diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index 022d1f6d6..cf0d788a3 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -15,7 +15,7 @@ use super::manager::{PublicationUpdatesEvent, SubscriberHandlesEvent, SubscriptionUpdatedEvent}; use crate::{ api::{DataTrackInfo, DataTrackSid, InternalError}, - dtp::Handle, + packet::Handle, }; use livekit_protocol::{self as proto, ParticipantInfo}; use std::{collections::HashMap, mem}; diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index b43125ed3..5f07daf44 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::dtp::Handle; +use crate::packet::Handle; use from_variants::FromVariants; use std::{fmt::Display, marker::PhantomData, ops::Deref, sync::Arc}; use thiserror::Error; diff --git a/livekit-datatrack/src/utils/handle_map.rs b/livekit-datatrack/src/utils/handle_map.rs index a009cbaaf..c1396c37b 100644 --- a/livekit-datatrack/src/utils/handle_map.rs +++ b/livekit-datatrack/src/utils/handle_map.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{api::DataTrackSid, dtp::Handle}; +use crate::{api::DataTrackSid, packet::Handle}; use std::collections::HashMap; /// Map between track handle and SID. From ea9166391326ea36ccbf2d923778724999747f2b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:30:55 +1100 Subject: [PATCH 179/232] Apply clippy suggestions --- livekit-datatrack/src/local/pipeline.rs | 2 +- livekit-datatrack/src/remote/manager.rs | 2 +- livekit-datatrack/src/remote/proto.rs | 4 ++-- livekit-datatrack/src/utils/handle_map.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 0ebef42ce..de640bfca 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -126,7 +126,7 @@ impl From for PacketizerFrame { Self { payload: frame.payload, extensions: Extensions { - user_timestamp: frame.user_timestamp.map(|v| UserTimestampExt(v)), + user_timestamp: frame.user_timestamp.map(UserTimestampExt), e2ee: None, }, } diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 622d1d5f7..36598f8cf 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -334,7 +334,7 @@ impl Manager { for (_, descriptor) in self.descriptors { _ = descriptor.state_tx.send(TrackState::Unpublished); match descriptor.state { - DescriptorState::Available { .. } => {} + DescriptorState::Available => {} DescriptorState::PendingSubscriberHandle { result_txs } => { for result_tx in result_txs { _ = result_tx.send(Err(SubscribeError::Disconnected)); diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index cf0d788a3..530a7a808 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -69,13 +69,13 @@ pub fn event_from_participant_update( /// participant identity is set. /// fn event_from_participant_info( - msg: &mut Vec, + msg: &mut [ParticipantInfo], local_participant_identity: Option<&str>, ) -> Result { let updates = msg .iter_mut() .filter(|participant| { - local_participant_identity.map_or(true, |identity| participant.identity != identity) + local_participant_identity.is_none_or(|identity| participant.identity != identity) }) .map(|participant| -> Result<_, InternalError> { Ok((participant.identity.clone(), extract_track_info(participant)?)) diff --git a/livekit-datatrack/src/utils/handle_map.rs b/livekit-datatrack/src/utils/handle_map.rs index c1396c37b..fa02e12c3 100644 --- a/livekit-datatrack/src/utils/handle_map.rs +++ b/livekit-datatrack/src/utils/handle_map.rs @@ -37,7 +37,7 @@ impl HandleMap { } self.sid_to_handle.insert(sid.clone(), handle); self.handle_to_sid.insert(handle, sid); - return true; + true } /// Get the SID associated with the given handle. From 0126d93493292da9c8ef4e67814948b47a2ff27e Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:31:41 +1100 Subject: [PATCH 180/232] Format --- livekit-datatrack/src/lib.rs | 2 +- livekit-datatrack/src/local/manager.rs | 14 +++++++------- livekit-datatrack/src/local/packetizer.rs | 2 +- livekit-datatrack/src/local/pipeline.rs | 12 ++++-------- livekit-datatrack/src/local/proto.rs | 2 +- livekit-datatrack/src/packet/deserialize.rs | 2 +- livekit-datatrack/src/packet/extension.rs | 2 +- livekit-datatrack/src/packet/handle.rs | 2 +- livekit-datatrack/src/packet/serialize.rs | 6 ++++-- livekit-datatrack/src/packet/time.rs | 2 +- livekit-datatrack/src/remote/depacketizer.rs | 2 +- livekit-datatrack/src/remote/manager.rs | 7 +++++-- livekit-datatrack/src/remote/pipeline.rs | 2 +- livekit-datatrack/src/track.rs | 2 +- livekit-datatrack/src/utils/bytes.rs | 5 +---- 15 files changed, 31 insertions(+), 33 deletions(-) diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index d13791ce9..d43071130 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -50,4 +50,4 @@ pub mod internal { pub mod remote { pub use crate::remote::{manager::*, proto::*}; } -} \ No newline at end of file +} diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 2891dcd3e..a8206b8a4 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -12,12 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{pipeline::{Pipeline, PipelineOptions}, LocalTrackInner}; +use super::{ + pipeline::{Pipeline, PipelineOptions}, + LocalTrackInner, +}; use crate::{ api::{DataTrackInfo, DataTrackOptions, InternalError, PublishError}, - packet::{self, Handle}, e2ee::EncryptionProvider, local::LocalDataTrack, + packet::{self, Handle}, }; use anyhow::{anyhow, Context}; use bytes::Bytes; @@ -221,11 +224,8 @@ impl Manager { let (state_tx, state_rx) = watch::channel(LocalTrackState::Published); let info = Arc::new(info); - let e2ee_provider = if info.uses_e2ee() { - self.e2ee_provider.as_ref().map(Arc::clone) - } else { - None - }; + let e2ee_provider = + if info.uses_e2ee() { self.e2ee_provider.as_ref().map(Arc::clone) } else { None }; let options = PipelineOptions { e2ee_provider, info: info.clone(), diff --git a/livekit-datatrack/src/local/packetizer.rs b/livekit-datatrack/src/local/packetizer.rs index f36728ff9..cf3f106f8 100644 --- a/livekit-datatrack/src/local/packetizer.rs +++ b/livekit-datatrack/src/local/packetizer.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::{ - packet::{Clock, Packet, Extensions, FrameMarker, Handle, Header, Timestamp}, + packet::{Clock, Extensions, FrameMarker, Handle, Header, Packet, Timestamp}, utils::{BytesChunkExt, Counter}, }; use bytes::Bytes; diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index de640bfca..88ec7690b 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -15,9 +15,9 @@ use super::packetizer::{Packetizer, PacketizerFrame}; use crate::{ api::{DataTrackFrame, DataTrackInfo}, - packet::{self, Extensions, UserTimestampExt}, e2ee::EncryptionProvider, local::manager::{LocalTrackState, OutputEvent, UnpublishInitiator, UnpublishRequestEvent}, + packet::{self, Extensions, UserTimestampExt}, }; use std::sync::Arc; use tokio::sync::{mpsc, watch}; @@ -166,19 +166,15 @@ mod tests { async fn test_publish_frame() { let (_, frame_tx, mut event_out_rx) = make_pipeline(); - let frame = DataTrackFrame { - payload: Bytes::from(vec![0xFA; 256]), - user_timestamp: Faker.fake() - }; + let frame = + DataTrackFrame { payload: Bytes::from(vec![0xFA; 256]), user_timestamp: Faker.fake() }; frame_tx.send(frame).await.unwrap(); while let Some(out_event) = event_out_rx.recv().await { let OutputEvent::PacketsAvailable(packets) = out_event else { panic!("Unexpected event") }; - let Some(packet) = packets.first() else { - panic!("Expected one packet") - }; + let Some(packet) = packets.first() else { panic!("Expected one packet") }; assert!(!packet.is_empty()); break; } diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index 143e96dc3..39aca1f2d 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -15,8 +15,8 @@ use super::manager::{PublishRequestEvent, UnpublishEvent, UnpublishRequestEvent}; use crate::{ api::{DataTrackInfo, DataTrackSid, InternalError, PublishError}, - packet::Handle, local::manager::PublishResultEvent, + packet::Handle, }; use anyhow::{anyhow, Context}; use livekit_protocol as proto; diff --git a/livekit-datatrack/src/packet/deserialize.rs b/livekit-datatrack/src/packet/deserialize.rs index cd78db3ad..6dd9a695b 100644 --- a/livekit-datatrack/src/packet/deserialize.rs +++ b/livekit-datatrack/src/packet/deserialize.rs @@ -13,7 +13,7 @@ // limitations under the License. use super::{ - consts::*, Packet, E2eeExt, ExtensionTag, Extensions, FrameMarker, Handle, HandleError, Header, + consts::*, E2eeExt, ExtensionTag, Extensions, FrameMarker, Handle, HandleError, Header, Packet, Timestamp, UserTimestampExt, }; use bytes::{Buf, Bytes}; diff --git a/livekit-datatrack/src/packet/extension.rs b/livekit-datatrack/src/packet/extension.rs index e5150445a..e4d82913f 100644 --- a/livekit-datatrack/src/packet/extension.rs +++ b/livekit-datatrack/src/packet/extension.rs @@ -49,4 +49,4 @@ impl UserTimestampExt { impl E2eeExt { pub(super) const TAG: ExtensionTag = 1; pub(super) const LEN: usize = 13; -} \ No newline at end of file +} diff --git a/livekit-datatrack/src/packet/handle.rs b/livekit-datatrack/src/packet/handle.rs index 2b1372d06..88ef700be 100644 --- a/livekit-datatrack/src/packet/handle.rs +++ b/livekit-datatrack/src/packet/handle.rs @@ -86,4 +86,4 @@ impl fake::Dummy for Handle { fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { Self::try_from(rng.random_range(1..u16::MAX)).unwrap() } -} \ No newline at end of file +} diff --git a/livekit-datatrack/src/packet/serialize.rs b/livekit-datatrack/src/packet/serialize.rs index 0149ed206..a175d2835 100644 --- a/livekit-datatrack/src/packet/serialize.rs +++ b/livekit-datatrack/src/packet/serialize.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{consts::*, Packet, E2eeExt, Extensions, FrameMarker, Header, UserTimestampExt}; +use super::{consts::*, E2eeExt, Extensions, FrameMarker, Header, Packet, UserTimestampExt}; use bytes::{BufMut, Bytes, BytesMut}; use thiserror::Error; @@ -164,7 +164,9 @@ impl UserTimestampExt { #[cfg(test)] mod tests { - use crate::packet::{Packet, E2eeExt, Extensions, FrameMarker, Header, Timestamp, UserTimestampExt}; + use crate::packet::{ + E2eeExt, Extensions, FrameMarker, Header, Packet, Timestamp, UserTimestampExt, + }; use bytes::Buf; /// Constructed packet to use in tests. diff --git a/livekit-datatrack/src/packet/time.rs b/livekit-datatrack/src/packet/time.rs index fdcd88578..aba02f8d7 100644 --- a/livekit-datatrack/src/packet/time.rs +++ b/livekit-datatrack/src/packet/time.rs @@ -120,4 +120,4 @@ impl fake::Dummy for Timestamp { fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { Self(rng.random()) } -} \ No newline at end of file +} diff --git a/livekit-datatrack/src/remote/depacketizer.rs b/livekit-datatrack/src/remote/depacketizer.rs index c3261be31..2a5aabe53 100644 --- a/livekit-datatrack/src/remote/depacketizer.rs +++ b/livekit-datatrack/src/remote/depacketizer.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::packet::{Packet, Extensions, FrameMarker}; +use crate::packet::{Extensions, FrameMarker, Packet}; use bytes::{Bytes, BytesMut}; use std::collections::BTreeMap; diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 36598f8cf..491ed3236 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -12,11 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{pipeline::{Pipeline, PipelineOptions}, RemoteDataTrack, RemoteTrackInner}; +use super::{ + pipeline::{Pipeline, PipelineOptions}, + RemoteDataTrack, RemoteTrackInner, +}; use crate::{ api::{DataTrackFrame, DataTrackInfo, DataTrackSid, InternalError, SubscribeError}, - packet::{Packet, Handle}, e2ee::DecryptionProvider, + packet::{Handle, Packet}, utils::HandleMap, }; use anyhow::{anyhow, Context}; diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 4a108388f..8a8bd2ab8 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -18,8 +18,8 @@ use super::{ }; use crate::{ api::{DataTrackFrame, DataTrackInfo}, - packet::Packet, e2ee::{DecryptionProvider, EncryptedPayload}, + packet::Packet, remote::depacketizer::DepacketizerFrame, }; use std::sync::Arc; diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index 5f07daf44..ecd5abd69 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -121,4 +121,4 @@ impl fake::Dummy for DataTrackSid { .collect(); Self::try_from(format!("{}{}", Self::PREFIX, random_id)).unwrap() } -} \ No newline at end of file +} diff --git a/livekit-datatrack/src/utils/bytes.rs b/livekit-datatrack/src/utils/bytes.rs index 8a2aaac32..6019359a2 100644 --- a/livekit-datatrack/src/utils/bytes.rs +++ b/livekit-datatrack/src/utils/bytes.rs @@ -65,10 +65,7 @@ mod tests { } #[test_matrix([1, 128, 333], [1, 64, 128, 256, 123])] - fn test_chunks( - chunk_size: usize, - source_size: usize, - ) { + fn test_chunks(chunk_size: usize, source_size: usize) { let source = Bytes::from(vec![0xCC; source_size]); let chunks: Vec<_> = source.into_chunks(chunk_size).collect(); From b8d537d3c807c426a8caebc28eb80858ca605f0e Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:20:01 +1100 Subject: [PATCH 181/232] Fix spelling error --- examples/basic_data_track/src/subscriber.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/basic_data_track/src/subscriber.rs b/examples/basic_data_track/src/subscriber.rs index 7d906273d..78cd8cbb5 100644 --- a/examples/basic_data_track/src/subscriber.rs +++ b/examples/basic_data_track/src/subscriber.rs @@ -39,8 +39,8 @@ async fn subscribe(track: RemoteDataTrack) -> Result<()> { track.info().name(), track.publisher_identity() ); - let mut frame_steam = track.subscribe().await?; - while let Some(frame) = frame_steam.next().await { + let mut stream = track.subscribe().await?; + while let Some(frame) = stream.next().await { log::info!("Received frame ({} bytes)", frame.payload().len()); } log::info!("Unsubscribed"); From d003f6e0dbcdd6d26ba7b27f5499332c26de4821 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:57:46 +1100 Subject: [PATCH 182/232] Decouple pipeline from task --- livekit-datatrack/src/local/manager.rs | 147 ++++++++++++++--------- livekit-datatrack/src/local/mod.rs | 10 +- livekit-datatrack/src/local/pipeline.rs | 151 +++++++----------------- livekit-datatrack/src/local/proto.rs | 2 +- 4 files changed, 137 insertions(+), 173 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index a8206b8a4..fca885b90 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -17,7 +17,7 @@ use super::{ LocalTrackInner, }; use crate::{ - api::{DataTrackInfo, DataTrackOptions, InternalError, PublishError}, + api::{DataTrackFrame, DataTrackInfo, DataTrackOptions, InternalError, PublishError}, e2ee::EncryptionProvider, local::LocalDataTrack, packet::{self, Handle}, @@ -59,12 +59,13 @@ pub struct PublishResultEvent { pub result: Result, } -/// SFU notification that a track published by the local participant -/// has been unpublished. +/// Track has been unpublished. #[derive(Debug)] pub struct UnpublishEvent { /// Publisher handle of the track that was unpublished. pub handle: Handle, + /// Whether the unpublish was initiated by the client. + pub client_initiated: bool, } /// Local participant requested to publish a track. @@ -115,7 +116,7 @@ pub struct ManagerOptions { /// System for managing data track publications. pub struct Manager { e2ee_provider: Option>, - event_in_tx: mpsc::WeakSender, + event_in_tx: mpsc::Sender, event_in_rx: mpsc::Receiver, event_out_tx: mpsc::Sender, handle_allocator: packet::HandleAllocator, @@ -138,7 +139,7 @@ impl Manager { let event_in = ManagerInput { event_in_tx: event_in_tx.clone() }; let manager = Manager { e2ee_provider: options.e2ee_provider, - event_in_tx: event_in_tx.downgrade(), + event_in_tx, event_in_rx, event_out_tx, handle_allocator: packet::HandleAllocator::default(), @@ -158,9 +159,9 @@ impl Manager { while let Some(event) = self.event_in_rx.recv().await { log::debug!("Input event: {:?}", event); match event { - InputEvent::Publish(event) => self.handle_publish(event), - InputEvent::PublishResult(event) => self.handle_publish_result(event), - InputEvent::Unpublish(event) => self.handle_unpublished(event), + InputEvent::Publish(event) => self.handle_publish(event).await, + InputEvent::PublishResult(event) => self.handle_publish_result(event).await, + InputEvent::Unpublish(event) => self.handle_unpublished(event).await, InputEvent::Shutdown => break, } } @@ -168,7 +169,7 @@ impl Manager { log::debug!("Task ended"); } - fn handle_publish(&mut self, event: PublishEvent) { + async fn handle_publish(&mut self, event: PublishEvent) { let Some(handle) = self.handle_allocator.get() else { _ = event.result_tx.send(Err(PublishError::LimitReached)); return; @@ -187,12 +188,12 @@ impl Manager { name: event.options.name, uses_e2ee: self.e2ee_provider.is_some() && !event.options.disable_e2ee, }; - _ = self.event_out_tx.try_send(publish_requested.into()); // TODO: check for error. + _ = self.event_out_tx.send(publish_requested.into()).await; self.schedule_publish_timeout(handle); } fn schedule_publish_timeout(&self, handle: Handle) { - let event_in_tx = self.event_in_tx.clone(); + let event_in_tx = self.event_in_tx.downgrade(); let emit_timeout = async move { time::sleep(Self::PUBLISH_TIMEOUT).await; let Some(tx) = event_in_tx.upgrade() else { return }; @@ -202,7 +203,7 @@ impl Manager { livekit_runtime::spawn(emit_timeout); } - fn handle_publish_result(&mut self, event: PublishResultEvent) { + async fn handle_publish_result(&mut self, event: PublishResultEvent) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { log::warn!("No descriptor for {}", event.handle); return; @@ -220,44 +221,50 @@ impl Manager { } fn create_local_track(&mut self, info: DataTrackInfo) -> LocalDataTrack { - let (frame_tx, frame_rx) = mpsc::channel(4); // TODO: tune - let (state_tx, state_rx) = watch::channel(LocalTrackState::Published); let info = Arc::new(info); - let e2ee_provider = if info.uses_e2ee() { self.e2ee_provider.as_ref().map(Arc::clone) } else { None }; - let options = PipelineOptions { - e2ee_provider, + + let pipeline_opts = PipelineOptions { e2ee_provider, info: info.clone() }; + let pipeline = Pipeline::new(pipeline_opts); + + let (frame_tx, frame_rx) = mpsc::channel(4); // TODO: tune + let (published_tx, published_rx) = watch::channel(true); + + let track_task = TrackTask { info: info.clone(), + pipeline, + published_rx, frame_rx, - state_rx, - event_out_tx: self.event_out_tx.downgrade(), + event_in_tx: self.event_in_tx.clone(), + event_out_tx: self.event_out_tx.clone(), }; - let pipeline = Pipeline::new(options); - let pipeline_handle = livekit_runtime::spawn(pipeline.run()); + let task_handle = livekit_runtime::spawn(track_task.run()); self.descriptors.insert( info.pub_handle, - Descriptor::Active { state_tx: state_tx.clone(), pipeline_handle }, + Descriptor::Active { published_tx: published_tx.clone(), task_handle }, ); - let inner = LocalTrackInner { frame_tx, state_tx }; + let inner = LocalTrackInner { frame_tx, published_tx }; LocalDataTrack::new(info, inner) } - fn handle_unpublished(&mut self, event: UnpublishEvent) { + async fn handle_unpublished(&mut self, event: UnpublishEvent) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { - log::warn!("No descriptor for track {}", event.handle); return; }; - let Descriptor::Active { state_tx, .. } = descriptor else { - log::warn!("Cannot unpublish pending track {}", event.handle); + let Descriptor::Active { published_tx, .. } = descriptor else { return; }; - if !state_tx.borrow().is_published() { - return; + if !*published_tx.borrow() { + _ = published_tx.send(false); + } + if event.client_initiated { + // Inform the SFU if client initiated. + let event = UnpublishRequestEvent { handle: event.handle }; + _ = self.event_out_tx.send(event.into()).await; } - _ = state_tx.send(LocalTrackState::Unpublished { initiator: UnpublishInitiator::Sfu }); } /// Performs cleanup before the task ends. @@ -267,11 +274,9 @@ impl Manager { Descriptor::Pending(result_tx) => { _ = result_tx.send(Err(PublishError::Disconnected)) } - Descriptor::Active { state_tx, pipeline_handle } => { - _ = state_tx.send(LocalTrackState::Unpublished { - initiator: UnpublishInitiator::Shutdown, - }); - pipeline_handle.await; + Descriptor::Active { published_tx, task_handle } => { + _ = published_tx.send(false); + task_handle.await; } } } @@ -281,6 +286,52 @@ impl Manager { const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); } +/// Task for an individual published data track. +struct TrackTask { + info: Arc, + pipeline: Pipeline, + published_rx: watch::Receiver, + frame_rx: mpsc::Receiver, + event_in_tx: mpsc::Sender, + event_out_tx: mpsc::Sender, +} + +impl TrackTask { + async fn run(mut self) { + log::debug!("Track task started: sid={}", self.info.sid); + + let mut is_published = *self.published_rx.borrow(); + while is_published { + tokio::select! { + _ = self.published_rx.changed() => { + is_published = *self.published_rx.borrow(); + } + Some(frame) = self.frame_rx.recv() => self.process_and_send(frame) + } + } + + let event = UnpublishEvent { handle: self.info.pub_handle, client_initiated: true }; + _ = self.event_in_tx.send(event.into()).await; + + log::debug!("Track task ended: sid={}", self.info.sid); + } + + fn process_and_send(&mut self, frame: DataTrackFrame) { + let Ok(packets) = self + .pipeline + .process_frame(frame) + .inspect_err(|err| log::debug!("Process failed: {}", err)) + else { + return; + }; + let packets: Vec<_> = packets.into_iter().map(|packet| packet.serialize()).collect(); + _ = self + .event_out_tx + .try_send(packets.into()) + .inspect_err(|err| log::debug!("Cannot send packet to transport: {}", err)); + } +} + #[derive(Debug)] enum Descriptor { /// Publication is awaiting SFU response. @@ -291,31 +342,9 @@ enum Descriptor { Pending(oneshot::Sender>), /// Publication is active. /// - /// The associated channel is used to send state updates to the track's task. + /// The associated channel is used to end the track task. /// - Active { - state_tx: watch::Sender, - pipeline_handle: livekit_runtime::JoinHandle<()>, - }, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) enum UnpublishInitiator { - Client, - Sfu, - Shutdown, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) enum LocalTrackState { - Published, - Unpublished { initiator: UnpublishInitiator }, -} - -impl LocalTrackState { - pub fn is_published(&self) -> bool { - matches!(self, Self::Published) - } + Active { published_tx: watch::Sender, task_handle: livekit_runtime::JoinHandle<()> }, } /// Channel for sending [`InputEvent`]s to [`Manager`]. diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 9f9347900..755246763 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -16,7 +16,6 @@ use crate::{ api::{DataTrack, DataTrackFrame, DataTrackInfo, InternalError}, track::DataTrackInner, }; -use manager::{LocalTrackState, UnpublishInitiator}; use std::{fmt, marker::PhantomData, sync::Arc}; use thiserror::Error; use tokio::sync::{mpsc, watch}; @@ -94,7 +93,7 @@ impl DataTrack { /// Once the track has been unpublished, calls to [`Self::publish`] will fail. /// pub fn is_published(&self) -> bool { - self.inner().state_tx.borrow().is_published() + *self.inner().published_tx.borrow() } /// Unpublishes the track. @@ -106,15 +105,12 @@ impl DataTrack { #[derive(Debug, Clone)] pub(crate) struct LocalTrackInner { pub frame_tx: mpsc::Sender, - pub state_tx: watch::Sender, + pub published_tx: watch::Sender, } impl LocalTrackInner { fn local_unpublish(&self) { - self.state_tx - .send(LocalTrackState::Unpublished { initiator: UnpublishInitiator::Client }) - .inspect_err(|err| log::error!("Failed to update state to unsubscribed: {err}")) - .ok(); + _ = self.published_tx.send(false); } } diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 88ec7690b..8aa549dd5 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -15,30 +15,31 @@ use super::packetizer::{Packetizer, PacketizerFrame}; use crate::{ api::{DataTrackFrame, DataTrackInfo}, - e2ee::EncryptionProvider, - local::manager::{LocalTrackState, OutputEvent, UnpublishInitiator, UnpublishRequestEvent}, - packet::{self, Extensions, UserTimestampExt}, + e2ee::{EncryptionError, EncryptionProvider}, + local::packetizer::PacketizerError, + packet::{self, Extensions, Packet, UserTimestampExt}, }; +use from_variants::FromVariants; use std::sync::Arc; -use tokio::sync::{mpsc, watch}; - +use thiserror::Error; /// Options for creating a [`Pipeline`]. pub(super) struct PipelineOptions { - pub e2ee_provider: Option>, pub info: Arc, - pub state_rx: watch::Receiver, - pub frame_rx: mpsc::Receiver, - pub event_out_tx: mpsc::WeakSender, + pub e2ee_provider: Option>, } /// Pipeline for an individual published data track. pub(super) struct Pipeline { - packetizer: Packetizer, e2ee_provider: Option>, - info: Arc, - state_rx: watch::Receiver, - frame_rx: mpsc::Receiver, - event_out_tx: mpsc::WeakSender, + packetizer: Packetizer, +} + +#[derive(Debug, Error, FromVariants)] +pub(super) enum PipelineError { + #[error(transparent)] + Packetizer(PacketizerError), + #[error(transparent)] + Encryption(EncryptionError), } impl Pipeline { @@ -46,75 +47,30 @@ impl Pipeline { pub fn new(options: PipelineOptions) -> Self { debug_assert_eq!(options.info.uses_e2ee, options.e2ee_provider.is_some()); let packetizer = Packetizer::new(options.info.pub_handle, Self::TRANSPORT_MTU); - Self { - packetizer, - e2ee_provider: options.e2ee_provider, - info: options.info, - state_rx: options.state_rx, - frame_rx: options.frame_rx, - event_out_tx: options.event_out_tx, - } - } - - /// Run the pipeline task, consuming self. - pub async fn run(mut self) { - log::debug!("Pipeline task started: sid={}", self.info.sid); - let mut state = *self.state_rx.borrow(); - while state.is_published() { - tokio::select! { - biased; // State updates take priority - _ = self.state_rx.changed() => { - state = *self.state_rx.borrow(); - }, - Some(frame) = self.frame_rx.recv() => { - self.publish_frame(frame); - }, - else => break - } - } - if let LocalTrackState::Unpublished { initiator: UnpublishInitiator::Client } = state { - let event = UnpublishRequestEvent { handle: self.info.pub_handle }; - if let Some(event_out_tx) = self.event_out_tx.upgrade() { - _ = event_out_tx.try_send(event.into()); - } - } - log::debug!("Pipeline task ended: sid={}", self.info.sid); + Self { e2ee_provider: options.e2ee_provider, packetizer } } - fn publish_frame(&mut self, frame: DataTrackFrame) { - let Some(frame) = self.encrypt_if_needed(frame.into()) else { return }; - - let packets = match self.packetizer.packetize(frame) { - Ok(packets) => packets, - Err(err) => { - log::error!("Failed to packetize frame: {}", err); - return; - } - }; - let packets: Vec<_> = packets.into_iter().map(|packet| packet.serialize()).collect(); - if let Some(event_out_tx) = self.event_out_tx.upgrade() { - _ = event_out_tx - .try_send(packets.into()) - .inspect_err(|err| log::debug!("Cannot send packet to transport: {}", err)); - } + pub fn process_frame(&mut self, frame: DataTrackFrame) -> Result, PipelineError> { + let frame = self.encrypt_if_needed(frame.into())?; + let frame = self.packetizer.packetize(frame)?; + Ok(frame) } /// Encrypt the frame's payload if E2EE is enabled for this track. - fn encrypt_if_needed(&self, mut frame: PacketizerFrame) -> Option { - let Some(e2ee_provider) = &self.e2ee_provider else { return frame.into() }; - - let encrypted = match e2ee_provider.encrypt(frame.payload) { - Ok(payload) => payload, - Err(err) => { - log::error!("{}", err); - return None; - } + fn encrypt_if_needed( + &self, + mut frame: PacketizerFrame, + ) -> Result { + let Some(e2ee_provider) = &self.e2ee_provider else { + return Ok(frame.into()); }; + let encrypted = e2ee_provider.encrypt(frame.payload)?; + frame.payload = encrypted.payload; frame.extensions.e2ee = packet::E2eeExt { key_index: encrypted.key_index, iv: encrypted.iv }.into(); - frame.into() + Ok(frame) } /// Maximum transmission unit (MTU) of the transport. @@ -136,47 +92,30 @@ impl From for PacketizerFrame { #[cfg(test)] mod tests { use super::*; - use bytes::Bytes; use fake::{Fake, Faker}; + use bytes::Bytes; - fn make_pipeline( - ) -> (watch::Sender, mpsc::Sender, mpsc::Receiver) - { - let (state_tx, state_rx) = watch::channel(LocalTrackState::Published); - let (frame_tx, frame_rx) = mpsc::channel(32); - let (event_out_tx, event_out_rx) = mpsc::channel(32); - + #[test] + fn test_process_frame() { let mut info: DataTrackInfo = Faker.fake(); info.uses_e2ee = false; - let options = PipelineOptions { - e2ee_provider: None, - info: info.into(), - state_rx, - frame_rx, - event_out_tx: event_out_tx.downgrade(), + let options = PipelineOptions { info: info.into(), e2ee_provider: None }; + let mut pipeline = Pipeline::new(options); + + let repeated_byte: u8 = Faker.fake(); + let frame = DataTrackFrame { + payload: Bytes::from(vec![repeated_byte; 32_000]), + user_timestamp: Faker.fake(), }; - let pipeline = Pipeline::new(options); - livekit_runtime::spawn(pipeline.run()); - (state_tx, frame_tx, event_out_rx) - } + let packets = pipeline.process_frame(frame).unwrap(); + assert_eq!(packets.len(), 3); - #[tokio::test] - async fn test_publish_frame() { - let (_, frame_tx, mut event_out_rx) = make_pipeline(); - - let frame = - DataTrackFrame { payload: Bytes::from(vec![0xFA; 256]), user_timestamp: Faker.fake() }; - frame_tx.send(frame).await.unwrap(); - - while let Some(out_event) = event_out_rx.recv().await { - let OutputEvent::PacketsAvailable(packets) = out_event else { - panic!("Unexpected event") - }; - let Some(packet) = packets.first() else { panic!("Expected one packet") }; - assert!(!packet.is_empty()); - break; + for packet in packets { + assert!(packet.header.extensions.e2ee.is_none()); + assert!(!packet.payload.is_empty()); + assert!(packet.payload.iter().all(|byte| *byte == repeated_byte)); } } } diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index 39aca1f2d..a1bd2423d 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -55,7 +55,7 @@ impl TryFrom for UnpublishEvent { fn try_from(msg: proto::UnpublishDataTrackResponse) -> Result { let handle: Handle = msg.info.context("Missing info")?.pub_handle.try_into().map_err(anyhow::Error::from)?; - Ok(Self { handle }) + Ok(Self { handle, client_initiated: false }) } } From 5b5df26b783955bdab1fdf1108de1a2b23467851 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:06:19 +1100 Subject: [PATCH 183/232] Fix header length calculation --- livekit-datatrack/src/packet/serialize.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/livekit-datatrack/src/packet/serialize.rs b/livekit-datatrack/src/packet/serialize.rs index a175d2835..3acd55ed1 100644 --- a/livekit-datatrack/src/packet/serialize.rs +++ b/livekit-datatrack/src/packet/serialize.rs @@ -65,7 +65,11 @@ struct HeaderMetrics { impl HeaderMetrics { fn serialized_len(&self) -> usize { - BASE_HEADER_LEN + EXT_WORDS_INDICATOR_SIZE + self.ext_len + self.padding_len + let mut len = BASE_HEADER_LEN; + if self.ext_len > 0 { + len += EXT_WORDS_INDICATOR_SIZE + self.ext_len + self.padding_len; + } + len } } @@ -86,6 +90,7 @@ impl Header { fn serialize_into(self, buf: &mut impl BufMut) -> Result { let metrics = self.metrics(); let serialized_len = metrics.serialized_len(); + let remaining_initial = buf.remaining_mut(); if buf.remaining_mut() < serialized_len { Err(SerializeError::TooSmallForHeader)? @@ -117,7 +122,7 @@ impl Header { buf.put_bytes(0, metrics.padding_len); } - // TODO: length assertion + assert_eq!(remaining_initial - buf.remaining_mut(), serialized_len); Ok(serialized_len) } } From 6f6d840b9b5d98e2a949d2b3c9c4167ccab61221 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:18:04 +1100 Subject: [PATCH 184/232] Doc --- livekit-datatrack/src/remote/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 8d3efc0e0..7b831600c 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -69,7 +69,7 @@ impl DataTrack { /// reuse the same underlying pipeline and do not trigger additional signaling. /// /// Note that newly created subscriptions only receive frames published after - /// the subscription is established. + /// the initial subscription is established. /// pub async fn subscribe(&self) -> Result, SubscribeError> { let (result_tx, result_rx) = oneshot::channel(); From 02c22d68f07b4fd578bf336b5204a670d4d2935e Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:24:57 +1100 Subject: [PATCH 185/232] Remove debug logs --- livekit-datatrack/src/remote/manager.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 491ed3236..ece50e0d8 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -151,7 +151,6 @@ impl Manager { pub async fn run(mut self) { log::debug!("Task started"); while let Some(event) = self.event_in_rx.recv().await { - log::debug!("Input event: {:?}", event); match event { InputEvent::PublicationUpdates(event) => { self.handle_publication_updates(event).await @@ -192,7 +191,6 @@ impl Manager { } async fn handle_track_published(&mut self, publisher_identity: String, info: DataTrackInfo) { - log::debug!("Track published: sid={}", info.sid); if self.descriptors.contains_key(&info.sid) { log::error!("Existing descriptor for track {}", info.sid); return; @@ -219,7 +217,6 @@ impl Manager { } fn handle_track_unpublished(&mut self, sid: DataTrackSid) { - log::debug!("Track unpublished: sid={}", sid); self.sub_handles.remove(&sid); let Some(descriptor) = self.descriptors.remove(&sid) else { log::error!("Unknown track {}", sid); From c4e53cdf6af5543b0984e0570cd325742e1d90cd Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:45:20 +1100 Subject: [PATCH 186/232] Decouple pipeline from task --- livekit-datatrack/src/remote/manager.rs | 58 +++++++++++++++++++--- livekit-datatrack/src/remote/pipeline.rs | 61 +++++------------------- 2 files changed, 62 insertions(+), 57 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index ece50e0d8..ccac1a3e9 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -284,19 +284,23 @@ impl Manager { None }; - let options = PipelineOptions { + let pipeline_opts = PipelineOptions { e2ee_provider, info: descriptor.info.clone(), publisher_identity: descriptor.publisher_identity.clone(), + }; + let pipeline = Pipeline::new(pipeline_opts); + + let track_task = TrackTask { + info: descriptor.info.clone(), + pipeline, state_rx: descriptor.state_tx.subscribe(), packet_rx, frame_tx: frame_tx.clone(), - event_out_tx: self.event_out_tx.downgrade(), }; - let pipeline = Pipeline::new(options); - let pipeline_handle = livekit_runtime::spawn(pipeline.run()); + let task_handle = livekit_runtime::spawn(track_task.run()); - descriptor.state = DescriptorState::Subscribed { packet_tx, frame_tx, pipeline_handle }; + descriptor.state = DescriptorState::Subscribed { packet_tx, frame_tx, task_handle }; self.sub_handles.insert(handle, sid); for result_tx in result_txs { @@ -340,7 +344,9 @@ impl Manager { _ = result_tx.send(Err(SubscribeError::Disconnected)); } } - DescriptorState::Subscribed { pipeline_handle, .. } => pipeline_handle.await, + DescriptorState::Subscribed { task_handle: pipeline_handle, .. } => { + pipeline_handle.await + } } } } @@ -354,6 +360,44 @@ struct Descriptor { state: DescriptorState, } +/// Task for an individual data track with an active subscription. +struct TrackTask { + info: Arc, + pipeline: Pipeline, + state_rx: watch::Receiver, + packet_rx: mpsc::Receiver, + frame_tx: broadcast::Sender, +} + +impl TrackTask { + async fn run(mut self) { + log::debug!("Task started: sid={}", self.info.sid); + let mut state = *self.state_rx.borrow(); + while state.is_published() { + tokio::select! { + biased; // State updates take priority + _ = self.state_rx.changed() => { + state = *self.state_rx.borrow(); + }, + Some(packet) = self.packet_rx.recv() => { + self.receive(packet); + }, + else => break + } + } + // TODO: handle unsubscribe + log::debug!("Task ended: sid={}", self.info.sid); + } + + fn receive(&mut self, packet: Packet) { + let Some(frame) = self.pipeline.process_packet(packet) else { return }; + _ = self + .frame_tx + .send(frame) + .inspect_err(|err| log::debug!("Cannot send frame to subscribers: {}", err)); + } +} + #[derive(Debug)] enum DescriptorState { Available, @@ -363,7 +407,7 @@ enum DescriptorState { Subscribed { packet_tx: mpsc::Sender, frame_tx: broadcast::Sender, - pipeline_handle: livekit_runtime::JoinHandle<()>, + task_handle: livekit_runtime::JoinHandle<()>, }, } diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 8a8bd2ab8..1b0642bb7 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -12,40 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{ - depacketizer::Depacketizer, - manager::{OutputEvent, TrackState}, -}; +use super::depacketizer::{Depacketizer, DepacketizerFrame}; use crate::{ api::{DataTrackFrame, DataTrackInfo}, e2ee::{DecryptionProvider, EncryptedPayload}, packet::Packet, - remote::depacketizer::DepacketizerFrame, }; use std::sync::Arc; -use tokio::sync::{broadcast, mpsc, watch}; /// Options for creating a [`Pipeline`]. pub(super) struct PipelineOptions { - pub e2ee_provider: Option>, pub info: Arc, pub publisher_identity: Arc, - pub state_rx: watch::Receiver, - pub packet_rx: mpsc::Receiver, - pub frame_tx: broadcast::Sender, - pub event_out_tx: mpsc::WeakSender, + pub e2ee_provider: Option>, } -/// Pipeline for an individual data track with an active subscription. +/// Pipeline for an individual data track subscription. pub(super) struct Pipeline { - depacketizer: Depacketizer, - e2ee_provider: Option>, - info: Arc, publisher_identity: Arc, - state_rx: watch::Receiver, - packet_rx: mpsc::Receiver, - frame_tx: broadcast::Sender, - event_out_tx: mpsc::WeakSender, + e2ee_provider: Option>, + depacketizer: Depacketizer, } impl Pipeline { @@ -54,41 +40,16 @@ impl Pipeline { debug_assert_eq!(options.info.uses_e2ee, options.e2ee_provider.is_some()); let depacketizer = Depacketizer::new(); Self { - depacketizer, - e2ee_provider: options.e2ee_provider, - info: options.info, publisher_identity: options.publisher_identity, - state_rx: options.state_rx, - packet_rx: options.packet_rx, - frame_tx: options.frame_tx, - event_out_tx: options.event_out_tx, - } - } - - /// Run the pipeline task, consuming self. - pub async fn run(mut self) { - log::debug!("Task started: sid={}", self.info.sid); - let mut state = *self.state_rx.borrow(); - while state.is_published() { - tokio::select! { - biased; // State updates take priority - _ = self.state_rx.changed() => { - state = *self.state_rx.borrow(); - }, - Some(packet) = self.packet_rx.recv() => { - self.receive_packet(packet); - }, - else => break - } + e2ee_provider: options.e2ee_provider, + depacketizer, } - log::debug!("Task ended: sid={}", self.info.sid); - // TODO: send unsubscribe if needed } - fn receive_packet(&mut self, packet: Packet) { - let Some(frame) = self.depacketizer.push(packet) else { return }; - let Some(frame) = self.decrypt_if_needed(frame) else { return }; - _ = self.frame_tx.send(frame.into()); + pub fn process_packet(&mut self, packet: Packet) -> Option { + let Some(frame) = self.depacketizer.push(packet) else { return None }; + let Some(frame) = self.decrypt_if_needed(frame) else { return None }; + Some(frame.into()) } /// Decrypt the frame's payload if E2EE is enabled for this track. From 41e80a5865234c70ccd92d9521705cd5993856cd Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:52:32 +1100 Subject: [PATCH 187/232] Simplify track state handling for remote manager --- livekit-datatrack/src/remote/manager.rs | 37 +++++++++---------------- livekit-datatrack/src/remote/mod.rs | 6 ++-- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index ccac1a3e9..ddb2ac348 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -198,17 +198,18 @@ impl Manager { let info = Arc::new(info); let publisher_identity: Arc = publisher_identity.into(); - let (state_tx, state_rx) = watch::channel(TrackState::Published); + let (published_tx, published_rx) = watch::channel(true); + let descriptor = Descriptor { info: info.clone(), publisher_identity: publisher_identity.clone(), - state_tx, + published_tx, state: DescriptorState::Available, }; self.descriptors.insert(descriptor.info.sid.clone(), descriptor); let inner = RemoteTrackInner { - state_rx, + published_rx, event_in_tx: self.event_in_tx.clone(), publisher_identity, }; @@ -222,7 +223,7 @@ impl Manager { log::error!("Unknown track {}", sid); return; }; - _ = descriptor.state_tx.send(TrackState::Unpublished); + _ = descriptor.published_tx.send(false); // TODO: this should end the track task } @@ -294,7 +295,7 @@ impl Manager { let track_task = TrackTask { info: descriptor.info.clone(), pipeline, - state_rx: descriptor.state_tx.subscribe(), + published_rx: descriptor.published_tx.subscribe(), packet_rx, frame_tx: frame_tx.clone(), }; @@ -336,7 +337,7 @@ impl Manager { /// Performs cleanup before the task ends. async fn shutdown(self) { for (_, descriptor) in self.descriptors { - _ = descriptor.state_tx.send(TrackState::Unpublished); + _ = descriptor.published_tx.send(false); match descriptor.state { DescriptorState::Available => {} DescriptorState::PendingSubscriberHandle { result_txs } => { @@ -356,7 +357,7 @@ impl Manager { struct Descriptor { info: Arc, publisher_identity: Arc, - state_tx: watch::Sender, + published_tx: watch::Sender, state: DescriptorState, } @@ -364,7 +365,7 @@ struct Descriptor { struct TrackTask { info: Arc, pipeline: Pipeline, - state_rx: watch::Receiver, + published_rx: watch::Receiver, packet_rx: mpsc::Receiver, frame_tx: broadcast::Sender, } @@ -372,12 +373,12 @@ struct TrackTask { impl TrackTask { async fn run(mut self) { log::debug!("Task started: sid={}", self.info.sid); - let mut state = *self.state_rx.borrow(); - while state.is_published() { + let mut is_published = *self.published_rx.borrow(); + while is_published { tokio::select! { biased; // State updates take priority - _ = self.state_rx.changed() => { - state = *self.state_rx.borrow(); + _ = self.published_rx.changed() => { + is_published = *self.published_rx.borrow(); }, Some(packet) = self.packet_rx.recv() => { self.receive(packet); @@ -411,18 +412,6 @@ enum DescriptorState { }, } -#[derive(Debug, Clone, Copy)] -pub(crate) enum TrackState { - Published, - Unpublished, -} - -impl TrackState { - pub fn is_published(&self) -> bool { - !matches!(self, Self::Unpublished) - } -} - /// Channel for sending [`InputEvent`]s to [`Manager`]. #[derive(Debug, Clone)] pub struct ManagerInput { diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 7b831600c..bbb152702 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -16,7 +16,7 @@ use crate::api::{DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, Inter use anyhow::anyhow; use futures_util::StreamExt; use livekit_runtime::timeout; -use manager::{SubscribeEvent, TrackState}; +use manager::SubscribeEvent; use std::{marker::PhantomData, sync::Arc, time::Duration}; use thiserror::Error; use tokio::sync::{mpsc, oneshot, watch}; @@ -101,7 +101,7 @@ impl DataTrack { /// result in an error. /// pub fn is_published(&self) -> bool { - self.inner().state_rx.borrow().is_published() + *self.inner().published_rx.borrow() } /// Identity of the participant who published the track. @@ -113,7 +113,7 @@ impl DataTrack { #[derive(Debug, Clone)] pub(crate) struct RemoteTrackInner { publisher_identity: Arc, - state_rx: watch::Receiver, + published_rx: watch::Receiver, event_in_tx: mpsc::WeakSender, } From d55004708e9b012a8a098af14a333807873f6931 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:58:19 +1100 Subject: [PATCH 188/232] Check publish status for any data track --- livekit-datatrack/src/local/mod.rs | 12 ++++-------- livekit-datatrack/src/remote/mod.rs | 15 ++++++--------- livekit-datatrack/src/track.rs | 8 ++++++++ 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 755246763..733f3131f 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -88,14 +88,6 @@ impl DataTrack { }) } - /// Whether or not the track is still published. - /// - /// Once the track has been unpublished, calls to [`Self::publish`] will fail. - /// - pub fn is_published(&self) -> bool { - *self.inner().published_tx.borrow() - } - /// Unpublishes the track. pub fn unpublish(self) { self.inner().local_unpublish(); @@ -109,6 +101,10 @@ pub(crate) struct LocalTrackInner { } impl LocalTrackInner { + pub fn is_published(&self) -> bool { + *self.published_tx.borrow() + } + fn local_unpublish(&self) { _ = self.published_tx.send(false); } diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index bbb152702..6dc33ba13 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -95,15 +95,6 @@ impl DataTrack { Ok(Box::pin(frame_stream)) } - /// Whether or not the track is still published. - /// - /// Once the track has been unpublished, calls to [`Self::subscribe()`] will - /// result in an error. - /// - pub fn is_published(&self) -> bool { - *self.inner().published_rx.borrow() - } - /// Identity of the participant who published the track. pub fn publisher_identity(&self) -> &str { &self.inner().publisher_identity @@ -117,6 +108,12 @@ pub(crate) struct RemoteTrackInner { event_in_tx: mpsc::WeakSender, } +impl RemoteTrackInner { + pub fn is_published(&self) -> bool { + *self.published_rx.borrow() + } +} + #[derive(Debug, Error)] pub enum SubscribeError { #[error("The track has been unpublished and is no longer available")] diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index ecd5abd69..c96e27136 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -37,6 +37,14 @@ impl DataTrack { pub fn info(&self) -> &DataTrackInfo { &self.info } + + /// Whether or not the track is still published. + pub fn is_published(&self) -> bool { + match self.inner.as_ref() { + DataTrackInner::Local(inner) => inner.is_published(), + DataTrackInner::Remote(inner) => inner.is_published(), + } + } } /// Information about a published data track. From c1c56ea9fd0f6051271e259e3e8dd01f902c1dde Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:31:40 +1100 Subject: [PATCH 189/232] Wait for unpublish method --- livekit-datatrack/src/local/mod.rs | 8 ++--- livekit-datatrack/src/remote/mod.rs | 4 +-- livekit-datatrack/src/track.rs | 25 +++++++++++++-- livekit/tests/data_track_test.rs | 49 +++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 733f3131f..dff6634f3 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -101,13 +101,13 @@ pub(crate) struct LocalTrackInner { } impl LocalTrackInner { - pub fn is_published(&self) -> bool { - *self.published_tx.borrow() - } - fn local_unpublish(&self) { _ = self.published_tx.send(false); } + + pub fn published_rx(&self) -> watch::Receiver { + self.published_tx.subscribe() + } } impl Drop for LocalTrackInner { diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 6dc33ba13..9ef159d0c 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -109,8 +109,8 @@ pub(crate) struct RemoteTrackInner { } impl RemoteTrackInner { - pub fn is_published(&self) -> bool { - *self.published_rx.borrow() + pub fn published_rx(&self) -> watch::Receiver { + self.published_rx.clone() } } diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index c96e27136..c9a4ae9e8 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -15,6 +15,7 @@ use crate::packet::Handle; use from_variants::FromVariants; use std::{fmt::Display, marker::PhantomData, ops::Deref, sync::Arc}; +use tokio::sync::watch; use thiserror::Error; /// Track for communicating application-specific data between participants in room. @@ -40,9 +41,29 @@ impl DataTrack { /// Whether or not the track is still published. pub fn is_published(&self) -> bool { + let published_rx = self.published_rx(); + let published = *published_rx.borrow(); + published + } + + /// Waits asynchronously until the track is unpublished. + /// + /// Use this to trigger follow-up work once the track is no longer published. + /// If the track is already unpublished, this method returns immediately. + /// + pub async fn wait_for_unpublish(&self) { + let mut published_rx = self.published_rx(); + if !*published_rx.borrow() { + // Already unpublished + return; + } + _ = published_rx.wait_for(|is_published| !*is_published).await; + } + + fn published_rx(&self) -> watch::Receiver { match self.inner.as_ref() { - DataTrackInner::Local(inner) => inner.is_published(), - DataTrackInner::Remote(inner) => inner.is_published(), + DataTrackInner::Local(inner) => inner.published_rx(), + DataTrackInner::Remote(inner) => inner.published_rx(), } } } diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 4b6a1b2b3..65918842b 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -253,3 +253,52 @@ async fn test_e2ee(disable_on_track: bool) -> Result<()> { timeout(Duration::from_secs(5), async { try_join!(publish, subscribe) }).await??; Ok(()) } + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_published_state() -> Result<()> { + // How long to leave the track published. + const PUBLISH_DURATION: Duration = Duration::from_millis(500); + + let mut rooms = test_rooms(2).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); + + let publish = async move { + let track = pub_room.local_participant().publish_data_track("my_track").await?; + + assert!(track.is_published()); + time::sleep(PUBLISH_DURATION).await; + track.unpublish(); + + Ok(()) + }; + + let subscribe = async move { + let track = async move { + while let Some(event) = sub_room_event_rx.recv().await { + let RoomEvent::RemoteDataTrackPublished(track) = event else { + continue; + }; + return Ok(track); + } + Err(anyhow!("No track published")) + } + .await?; + assert!(track.is_published()); + + let elapsed = { + let start = Instant::now(); + track.wait_for_unpublish().await; + start.elapsed() + }; + assert!(elapsed.abs_diff(PUBLISH_DURATION) <= Duration::from_millis(20),); + assert!(!track.is_published()); + + Ok(()) + }; + + timeout(Duration::from_secs(5), async { try_join!(publish, subscribe) }).await??; + Ok(()) +} From a3a1f061002bde0994b12877622f9a6ce402f657 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:40:04 +1100 Subject: [PATCH 190/232] Unsubscribe when no longer required --- livekit-datatrack/src/local/manager.rs | 4 +- livekit-datatrack/src/remote/manager.rs | 102 ++++++++++++++++++++---- 2 files changed, 89 insertions(+), 17 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index fca885b90..571499f35 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -378,7 +378,7 @@ impl ManagerInput { mod tests { use super::*; use crate::{api::DataTrackSid, packet::Packet}; - use fake::{faker::lorem::en::Word, Fake, Faker}; + use fake::{Fake, Faker}; use futures_util::StreamExt; use livekit_runtime::sleep; @@ -398,7 +398,7 @@ mod tests { let payload_size = 256; let packet_count = 10; - let track_name: String = Word().fake(); + let track_name: String = Faker.fake(); let track_sid: DataTrackSid = Faker.fake(); let pub_handle: Handle = Faker.fake(); diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index ddb2ac348..441baab6d 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -41,6 +41,7 @@ pub enum InputEvent { SubscriberHandles(SubscriberHandlesEvent), /// Packet has been received over the transport. PacketReceived(Bytes), + Unsubscribe(UnsubscribeEvent), /// Shutdown the manager, ending any subscriptions. Shutdown, } @@ -93,6 +94,13 @@ pub struct SubscriptionUpdatedEvent { pub subscribe: bool, } +/// Unsubscribe from a track. +#[derive(Debug)] +pub struct UnsubscribeEvent { + /// Identifier of the track to unsubscribe from. + sid: DataTrackSid, +} + /// Options for creating a [`Manager`]. #[derive(Debug)] pub struct ManagerOptions { @@ -107,7 +115,7 @@ pub struct ManagerOptions { /// System for managing data track subscriptions. pub struct Manager { e2ee_provider: Option>, - event_in_tx: mpsc::WeakSender, + event_in_tx: mpsc::Sender, event_in_rx: mpsc::Receiver, event_out_tx: mpsc::Sender, @@ -133,7 +141,7 @@ impl Manager { let event_in = ManagerInput { event_in_tx: event_in_tx.clone() }; let manager = Manager { e2ee_provider: options.e2ee_provider, - event_in_tx: event_in_tx.downgrade(), + event_in_tx, event_in_rx, event_out_tx, descriptors: HashMap::default(), @@ -158,6 +166,7 @@ impl Manager { InputEvent::Subscribe(event) => self.handle_subscribe(event).await, InputEvent::SubscriberHandles(event) => self.handle_subscriber_handles(event), InputEvent::PacketReceived(bytes) => self.handle_packet_received(bytes), + InputEvent::Unsubscribe(event) => self.handle_unsubscribe(event).await, InputEvent::Shutdown => break, } } @@ -210,7 +219,7 @@ impl Manager { let inner = RemoteTrackInner { published_rx, - event_in_tx: self.event_in_tx.clone(), + event_in_tx: self.event_in_tx.downgrade(), // TODO: wrap publisher_identity, }; let track = RemoteDataTrack::new(info, inner); @@ -298,6 +307,7 @@ impl Manager { published_rx: descriptor.published_tx.subscribe(), packet_rx, frame_tx: frame_tx.clone(), + event_in_tx: self.event_in_tx.clone(), }; let task_handle = livekit_runtime::spawn(track_task.run()); @@ -334,6 +344,20 @@ impl Manager { .inspect_err(|err| log::debug!("Cannot send packet to track pipeline: {}", err)); } + async fn handle_unsubscribe(&mut self, event: UnsubscribeEvent) { + let Some(descriptor) = self.descriptors.remove(&event.sid) else { + return; + }; + _ = descriptor.published_tx.send(false); + + let DescriptorState::Subscribed { .. } = &descriptor.state else { + log::warn!("Unexpected state"); + return; + }; + let event = SubscriptionUpdatedEvent { sid: event.sid, subscribe: false }; + _ = self.event_out_tx.send(event.into()).await; + } + /// Performs cleanup before the task ends. async fn shutdown(self) { for (_, descriptor) in self.descriptors { @@ -345,9 +369,7 @@ impl Manager { _ = result_tx.send(Err(SubscribeError::Disconnected)); } } - DescriptorState::Subscribed { task_handle: pipeline_handle, .. } => { - pipeline_handle.await - } + DescriptorState::Subscribed { task_handle, .. } => task_handle.await, } } } @@ -368,11 +390,13 @@ struct TrackTask { published_rx: watch::Receiver, packet_rx: mpsc::Receiver, frame_tx: broadcast::Sender, + event_in_tx: mpsc::Sender, } impl TrackTask { async fn run(mut self) { log::debug!("Task started: sid={}", self.info.sid); + let mut is_published = *self.published_rx.borrow(); while is_published { tokio::select! { @@ -380,13 +404,18 @@ impl TrackTask { _ = self.published_rx.changed() => { is_published = *self.published_rx.borrow(); }, + _ = self.frame_tx.closed() => { + let event = UnsubscribeEvent { sid: self.info.sid.clone() }; + _ = self.event_in_tx.send(event.into()).await; + break; // No more subscribers + }, Some(packet) = self.packet_rx.recv() => { self.receive(packet); }, else => break } } - // TODO: handle unsubscribe + log::debug!("Task ended: sid={}", self.info.sid); } @@ -428,16 +457,14 @@ impl ManagerInput { #[cfg(test)] mod tests { use super::*; - use fake::{ - faker::{internet::en::SafeEmail, lorem::en::Word}, - Fake, Faker, - }; - use futures_util::StreamExt; + use fake::{Fake, Faker}; + use futures_util::{future::join, StreamExt}; use std::{collections::HashMap, time::Duration}; + use test_case::test_case; use tokio::time; #[tokio::test] - async fn test_task_shutdown() { + async fn test_manager_task_shutdown() { let options = ManagerOptions { e2ee_provider: None }; let (manager, input, _) = Manager::new(options); @@ -447,10 +474,55 @@ mod tests { time::timeout(Duration::from_secs(1), join_handle).await.unwrap(); } + #[test_case(true; "via_unpublish")] + #[test_case(false; "via_unsubscribe")] + #[tokio::test] + async fn test_track_task_shutdown(via_unpublish: bool) { + let mut info: DataTrackInfo = Faker.fake(); + info.uses_e2ee = false; + + let info = Arc::new(info); + let sid = info.sid.clone(); + let publisher_identity: Arc = Faker.fake::().into(); + + let pipeline_opts = + PipelineOptions { info: info.clone(), publisher_identity, e2ee_provider: None }; + let pipeline = Pipeline::new(pipeline_opts); + + let (published_tx, published_rx) = watch::channel(true); + let (_packet_tx, packet_rx) = mpsc::channel(4); + let (frame_tx, frame_rx) = broadcast::channel(4); + let (event_in_tx, mut event_in_rx) = mpsc::channel(4); + + let task = + TrackTask { info: info, pipeline, published_rx, packet_rx, frame_tx, event_in_tx }; + let task_handle = livekit_runtime::spawn(task.run()); + + let trigger_shutdown = async { + if via_unpublish { + // Simulates SFU publication update + published_tx.send(false).unwrap(); + return; + } + // Simulates all subscribers dropped + mem::drop(frame_rx); + + while let Some(event) = event_in_rx.recv().await { + let InputEvent::Unsubscribe(event) = event else { + panic!("Unexpected event type"); + }; + assert_eq!(event.sid, sid); + return; + } + panic!("Did not receive unsubscribe"); + }; + time::timeout(Duration::from_secs(1), join(task_handle, trigger_shutdown)).await.unwrap(); + } + #[tokio::test] async fn test_subscribe() { - let publisher_identity: String = SafeEmail().fake(); - let track_name: String = Word().fake(); + let publisher_identity: String = Faker.fake(); + let track_name: String = Faker.fake(); let track_sid: DataTrackSid = Faker.fake(); let sub_handle: Handle = Faker.fake(); From 22a61fecd4af5ff5e7a63c5e49597f9def83cb76 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:50:01 +1100 Subject: [PATCH 191/232] Rename --- livekit-datatrack/src/local/manager.rs | 18 +++++++++--------- livekit-datatrack/src/local/pipeline.rs | 12 ++++++------ livekit-datatrack/src/remote/manager.rs | 18 +++++++++--------- livekit-datatrack/src/remote/pipeline.rs | 6 +++--- livekit/src/rtc_engine/rtc_session.rs | 4 ++-- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 571499f35..4b31692a6 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -110,12 +110,12 @@ pub struct ManagerOptions { /// /// If none, end-to-end encryption will be disabled for all published tracks. /// - pub e2ee_provider: Option>, + pub decryption_provider: Option>, } /// System for managing data track publications. pub struct Manager { - e2ee_provider: Option>, + encryption_provider: Option>, event_in_tx: mpsc::Sender, event_in_rx: mpsc::Receiver, event_out_tx: mpsc::Sender, @@ -138,7 +138,7 @@ impl Manager { let event_in = ManagerInput { event_in_tx: event_in_tx.clone() }; let manager = Manager { - e2ee_provider: options.e2ee_provider, + encryption_provider: options.decryption_provider, event_in_tx, event_in_rx, event_out_tx, @@ -186,7 +186,7 @@ impl Manager { let publish_requested = PublishRequestEvent { handle, name: event.options.name, - uses_e2ee: self.e2ee_provider.is_some() && !event.options.disable_e2ee, + uses_e2ee: self.encryption_provider.is_some() && !event.options.disable_e2ee, }; _ = self.event_out_tx.send(publish_requested.into()).await; self.schedule_publish_timeout(handle); @@ -222,10 +222,10 @@ impl Manager { fn create_local_track(&mut self, info: DataTrackInfo) -> LocalDataTrack { let info = Arc::new(info); - let e2ee_provider = - if info.uses_e2ee() { self.e2ee_provider.as_ref().map(Arc::clone) } else { None }; + let encryption_provider = + if info.uses_e2ee() { self.encryption_provider.as_ref().map(Arc::clone) } else { None }; - let pipeline_opts = PipelineOptions { e2ee_provider, info: info.clone() }; + let pipeline_opts = PipelineOptions { info: info.clone(), encryption_provider }; let pipeline = Pipeline::new(pipeline_opts); let (frame_tx, frame_rx) = mpsc::channel(4); // TODO: tune @@ -384,7 +384,7 @@ mod tests { #[tokio::test] async fn test_task_shutdown() { - let options = ManagerOptions { e2ee_provider: None }; + let options = ManagerOptions { decryption_provider: None }; let (manager, input, _) = Manager::new(options); let join_handle = livekit_runtime::spawn(manager.run()); @@ -402,7 +402,7 @@ mod tests { let track_sid: DataTrackSid = Faker.fake(); let pub_handle: Handle = Faker.fake(); - let options = ManagerOptions { e2ee_provider: None }; + let options = ManagerOptions { decryption_provider: None }; let (manager, input, mut output) = Manager::new(options); livekit_runtime::spawn(manager.run()); diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index 8aa549dd5..f723bdb2d 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -25,12 +25,12 @@ use thiserror::Error; /// Options for creating a [`Pipeline`]. pub(super) struct PipelineOptions { pub info: Arc, - pub e2ee_provider: Option>, + pub encryption_provider: Option>, } /// Pipeline for an individual published data track. pub(super) struct Pipeline { - e2ee_provider: Option>, + encryption_provider: Option>, packetizer: Packetizer, } @@ -45,9 +45,9 @@ pub(super) enum PipelineError { impl Pipeline { /// Creates a new pipeline with the given options. pub fn new(options: PipelineOptions) -> Self { - debug_assert_eq!(options.info.uses_e2ee, options.e2ee_provider.is_some()); + debug_assert_eq!(options.info.uses_e2ee, options.encryption_provider.is_some()); let packetizer = Packetizer::new(options.info.pub_handle, Self::TRANSPORT_MTU); - Self { e2ee_provider: options.e2ee_provider, packetizer } + Self { encryption_provider: options.encryption_provider, packetizer } } pub fn process_frame(&mut self, frame: DataTrackFrame) -> Result, PipelineError> { @@ -61,7 +61,7 @@ impl Pipeline { &self, mut frame: PacketizerFrame, ) -> Result { - let Some(e2ee_provider) = &self.e2ee_provider else { + let Some(e2ee_provider) = &self.encryption_provider else { return Ok(frame.into()); }; @@ -100,7 +100,7 @@ mod tests { let mut info: DataTrackInfo = Faker.fake(); info.uses_e2ee = false; - let options = PipelineOptions { info: info.into(), e2ee_provider: None }; + let options = PipelineOptions { info: info.into(), encryption_provider: None }; let mut pipeline = Pipeline::new(options); let repeated_byte: u8 = Faker.fake(); diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 441baab6d..3235d6049 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -109,12 +109,12 @@ pub struct ManagerOptions { /// If none, remote tracks using end-to-end encryption will not be available /// for subscription. /// - pub e2ee_provider: Option>, + pub encryption_provider: Option>, } /// System for managing data track subscriptions. pub struct Manager { - e2ee_provider: Option>, + decryption_provider: Option>, event_in_tx: mpsc::Sender, event_in_rx: mpsc::Receiver, event_out_tx: mpsc::Sender, @@ -140,7 +140,7 @@ impl Manager { let event_in = ManagerInput { event_in_tx: event_in_tx.clone() }; let manager = Manager { - e2ee_provider: options.e2ee_provider, + decryption_provider: options.encryption_provider, event_in_tx, event_in_rx, event_out_tx, @@ -288,16 +288,16 @@ impl Manager { let (packet_tx, packet_rx) = mpsc::channel(4); // TODO: tune let (frame_tx, frame_rx) = broadcast::channel(4); - let e2ee_provider = if descriptor.info.uses_e2ee() { - self.e2ee_provider.as_ref().map(Arc::clone) + let decryption_provider = if descriptor.info.uses_e2ee() { + self.decryption_provider.as_ref().map(Arc::clone) } else { None }; let pipeline_opts = PipelineOptions { - e2ee_provider, info: descriptor.info.clone(), publisher_identity: descriptor.publisher_identity.clone(), + decryption_provider, }; let pipeline = Pipeline::new(pipeline_opts); @@ -465,7 +465,7 @@ mod tests { #[tokio::test] async fn test_manager_task_shutdown() { - let options = ManagerOptions { e2ee_provider: None }; + let options = ManagerOptions { encryption_provider: None }; let (manager, input, _) = Manager::new(options); let join_handle = livekit_runtime::spawn(manager.run()); @@ -486,7 +486,7 @@ mod tests { let publisher_identity: Arc = Faker.fake::().into(); let pipeline_opts = - PipelineOptions { info: info.clone(), publisher_identity, e2ee_provider: None }; + PipelineOptions { info: info.clone(), publisher_identity, decryption_provider: None }; let pipeline = Pipeline::new(pipeline_opts); let (published_tx, published_rx) = watch::channel(true); @@ -526,7 +526,7 @@ mod tests { let track_sid: DataTrackSid = Faker.fake(); let sub_handle: Handle = Faker.fake(); - let options = ManagerOptions { e2ee_provider: None }; + let options = ManagerOptions { encryption_provider: None }; let (manager, input, mut output) = Manager::new(options); livekit_runtime::spawn(manager.run()); diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 1b0642bb7..669304056 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -24,7 +24,7 @@ use std::sync::Arc; pub(super) struct PipelineOptions { pub info: Arc, pub publisher_identity: Arc, - pub e2ee_provider: Option>, + pub decryption_provider: Option>, } /// Pipeline for an individual data track subscription. @@ -37,11 +37,11 @@ pub(super) struct Pipeline { impl Pipeline { /// Creates a new pipeline with the given options. pub fn new(options: PipelineOptions) -> Self { - debug_assert_eq!(options.info.uses_e2ee, options.e2ee_provider.is_some()); + debug_assert_eq!(options.info.uses_e2ee, options.decryption_provider.is_some()); let depacketizer = Depacketizer::new(); Self { publisher_identity: options.publisher_identity, - e2ee_provider: options.e2ee_provider, + e2ee_provider: options.decryption_provider, depacketizer, } } diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index efa7dea5d..5a15d55d4 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -485,7 +485,7 @@ impl RtcSession { rtc_events::forward_dc_events(&mut reliable_dc, DataPacketKind::Reliable, rtc_emitter); let local_dt_options = dt::local::ManagerOptions { - e2ee_provider: e2ee_manager.clone().filter(|m| m.enabled()).map(|m| { + decryption_provider: e2ee_manager.clone().filter(|m| m.enabled()).map(|m| { Arc::new(DataTrackEncryptionProvider::new(m, participant_info.identity.clone())) as Arc }), @@ -494,7 +494,7 @@ impl RtcSession { dt::local::Manager::new(local_dt_options); let remote_dt_options = dt::remote::ManagerOptions { - e2ee_provider: e2ee_manager.clone().filter(|m| m.enabled()).map(|m| { + encryption_provider: e2ee_manager.clone().filter(|m| m.enabled()).map(|m| { Arc::new(DataTrackDecryptionProvider::new(m)) as Arc }), }; From 7ce883c202f75558f72dd54a336a364f140fbf45 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:54:18 +1100 Subject: [PATCH 192/232] Unit test remote pipeline --- livekit-datatrack/src/remote/pipeline.rs | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index 669304056..cc15d9b73 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -82,3 +82,33 @@ impl From for DataTrackFrame { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::packet::{FrameMarker, Header}; + use fake::{Fake, Faker}; + + #[test] + fn test_process_frame() { + const PAYLOAD_LEN: usize = 1024; + + let mut info: DataTrackInfo = Faker.fake(); + info.uses_e2ee = false; + + let publisher_identity: Arc = Faker.fake::().into(); + + let options = + PipelineOptions { info: info.into(), publisher_identity, decryption_provider: None }; + let mut pipeline = Pipeline::new(options); + + let mut header: Header = Faker.fake(); + header.marker = FrameMarker::Single; + header.extensions.e2ee = None; + + let frame = Packet { header, payload: vec![Faker.fake(); PAYLOAD_LEN].into() }; + + let frame = pipeline.process_packet(frame).expect("Should return a frame"); + assert_eq!(frame.payload.len(), PAYLOAD_LEN); + } +} From 05c143afc4afb517d10f66d3f7a8f3123740fce3 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:18:36 +1100 Subject: [PATCH 193/232] User timestamp now method --- livekit-datatrack/src/frame.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/livekit-datatrack/src/frame.rs b/livekit-datatrack/src/frame.rs index fc4da3d01..ed8195e16 100644 --- a/livekit-datatrack/src/frame.rs +++ b/livekit-datatrack/src/frame.rs @@ -58,6 +58,16 @@ impl DataTrackFrame { self.user_timestamp = Some(value); self } + + /// Associates the current Unix timestamp (in milliseconds) with the frame. + pub fn with_user_timestamp_now(mut self) -> Self { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + self.user_timestamp = Some(timestamp); + self + } } impl fmt::Debug for DataTrackFrame { From 6f705bbd129e22de722cd09e357cb569657469b3 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:52:31 +1100 Subject: [PATCH 194/232] Rename publish frame to push --- examples/basic_data_track/src/publisher.rs | 8 ++--- livekit-datatrack/src/local/manager.rs | 2 +- livekit-datatrack/src/local/mod.rs | 40 ++++++++++------------ livekit/src/prelude.rs | 2 +- livekit/tests/data_track_test.rs | 6 ++-- 5 files changed, 28 insertions(+), 30 deletions(-) diff --git a/examples/basic_data_track/src/publisher.rs b/examples/basic_data_track/src/publisher.rs index d6ce29771..f152efc28 100644 --- a/examples/basic_data_track/src/publisher.rs +++ b/examples/basic_data_track/src/publisher.rs @@ -15,7 +15,7 @@ async fn main() -> Result<()> { let track = room.local_participant().publish_data_track("my_sensor_data").await?; tokio::select! { - _ = publish_frames(track) => {} + _ = push_frames(track) => {} _ = signal::ctrl_c() => {} } Ok(()) @@ -26,11 +26,11 @@ async fn read_sensor() -> Vec { vec![0xFA; 256] } -async fn publish_frames(track: LocalDataTrack) { +async fn push_frames(track: LocalDataTrack) { loop { - log::info!("Publishing frame"); + log::info!("Pushing frame"); let frame = read_sensor().await.into(); - track.publish(frame).inspect_err(|err| println!("Failed to publish frame: {}", err)).ok(); + track.try_push(frame).inspect_err(|err| println!("Failed to push frame: {}", err)).ok(); time::sleep(Duration::from_millis(500)).await } } diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 4b31692a6..419ffefa0 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -448,7 +448,7 @@ mod tests { assert_eq!(*track.info().sid(), track_sid); for _ in 0..packet_count { - track.publish(vec![0xFA; payload_size].into()).unwrap(); + track.try_push(vec![0xFA; payload_size].into()).unwrap(); sleep(Duration::from_millis(10)).await; } // Only reference to track dropped here (unpublish) diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index dff6634f3..5aca39b5c 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -50,20 +50,20 @@ impl DataTrack { } impl DataTrack { - /// Publishes a frame to the track. + /// Try pushing a frame to subscribers of the track. /// /// # Example /// /// ``` - /// # use livekit_datatrack::api::{LocalDataTrack, DataTrackFrame, PublishFrameError}; - /// # fn example(track: LocalDataTrack) -> Result<(), PublishFrameError> { + /// # use livekit_datatrack::api::{LocalDataTrack, DataTrackFrame, PushFrameError}; + /// # fn example(track: LocalDataTrack) -> Result<(), PushFrameError> { /// fn read_sensor() -> Vec { /// // Read some sensor data... /// vec![0xFA; 16] /// } /// /// let frame = read_sensor().into(); // Convert to frame - /// track.publish(frame)?; + /// track.try_push(frame)?; /// /// # Ok(()) /// # } @@ -73,18 +73,18 @@ impl DataTrack { /// /// # Errors /// - /// Publishing a frame can fail for several reasons: + /// Pushing a frame can fail for several reasons: /// /// - The track has been unpublished by the local participant or SFU /// - The room is no longer connected - /// - Frames are being published too fast + /// - Frames are being pushed too fast /// - pub fn publish(&self, frame: DataTrackFrame) -> Result<(), PublishFrameError> { + pub fn try_push(&self, frame: DataTrackFrame) -> Result<(), PushFrameError> { if !self.is_published() { - return Err(PublishFrameError::new(frame, PublishFrameErrorReason::TrackUnpublished)); + return Err(PushFrameError::new(frame, PushFrameErrorReason::TrackUnpublished)); } self.inner().frame_tx.try_send(frame).map_err(|err| { - PublishFrameError::new(err.into_inner(), PublishFrameErrorReason::Dropped) + PushFrameError::new(err.into_inner(), PushFrameErrorReason::Dropped) }) } @@ -203,22 +203,22 @@ pub enum PublishError { /// Frame could not be published to a data track. #[derive(Debug, Error)] #[error("Failed to publish frame: {reason}")] -pub struct PublishFrameError { +pub struct PushFrameError { frame: DataTrackFrame, - reason: PublishFrameErrorReason, + reason: PushFrameErrorReason, } -impl PublishFrameError { - pub(crate) fn new(frame: DataTrackFrame, reason: PublishFrameErrorReason) -> Self { +impl PushFrameError { + pub(crate) fn new(frame: DataTrackFrame, reason: PushFrameErrorReason) -> Self { Self { frame, reason } } - /// Returns the reason the frame could not be published. - pub fn reason(&self) -> PublishFrameErrorReason { + /// Returns the reason the frame could not be pushed. + pub fn reason(&self) -> PushFrameErrorReason { self.reason } - /// Consumes the error and returns the frame that couldn't be published. + /// Consumes the error and returns the frame that couldn't be pushed. /// /// This may be useful for implementing application-specific retry logic. /// @@ -227,18 +227,16 @@ impl PublishFrameError { } } -/// Reason why a data track frame could not be published. +/// Reason why a data track frame could not be pushed. #[derive(Debug, Clone, Copy)] -pub enum PublishFrameErrorReason { +pub enum PushFrameErrorReason { /// Track is no longer published. TrackUnpublished, /// Frame was dropped. Dropped, } -// TODO: could provide unpublish reason and more -// info about why the frame was dropped. -impl fmt::Display for PublishFrameErrorReason { +impl fmt::Display for PushFrameErrorReason { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::TrackUnpublished => write!(f, "track unpublished"), diff --git a/livekit/src/prelude.rs b/livekit/src/prelude.rs index 569d9d122..1f230e9e3 100644 --- a/livekit/src/prelude.rs +++ b/livekit/src/prelude.rs @@ -17,7 +17,7 @@ pub use livekit_protocol::AudioTrackFeature; pub use crate::{ data_track::{ DataTrackFrame, DataTrackInfo, DataTrackOptions, LocalDataTrack, PublishError, - PublishFrameError, PublishFrameErrorReason, RemoteDataTrack, + PushFrameError, PushFrameErrorReason, RemoteDataTrack, }, id::*, participant::{ diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 65918842b..7306807bd 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -60,7 +60,7 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { let sleep_duration = Duration::from_secs_f64(1.0 / publish_fps as f64); for index in 0..frame_count { - track.publish(vec![index as u8; payload_len].into())?; + track.try_push(vec![index as u8; payload_len].into())?; time::sleep(sleep_duration).await; } Ok(()) @@ -144,7 +144,7 @@ async fn test_publish_many_tracks() -> Result<()> { let tracks = timeout(Duration::from_secs(5), publish_tracks).await??; for track in &tracks { // Publish a single large frame per track. - track.publish(vec![0xFA; 196_608].into())?; + track.try_push(vec![0xFA; 196_608].into())?; } Ok(()) } @@ -221,7 +221,7 @@ async fn test_e2ee(disable_on_track: bool) -> Result<()> { assert!(track.info().uses_e2ee() || disable_on_track); for index in 0..5 { - track.publish(vec![index as u8; 196_608].into())?; + track.try_push(vec![index as u8; 196_608].into())?; time::sleep(Duration::from_millis(25)).await; } Ok(()) From 6442de1c73deffe2ad85767e37867f0d89830724 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:58:45 +1100 Subject: [PATCH 195/232] Remove per-track E2EE disable option --- livekit-datatrack/src/local/manager.rs | 2 +- livekit-datatrack/src/local/mod.rs | 18 ++++-------------- .../src/room/participant/local_participant.rs | 18 +----------------- 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 419ffefa0..e2a244d8c 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -186,7 +186,7 @@ impl Manager { let publish_requested = PublishRequestEvent { handle, name: event.options.name, - uses_e2ee: self.encryption_provider.is_some() && !event.options.disable_e2ee, + uses_e2ee: self.encryption_provider.is_some(), }; _ = self.event_out_tx.send(publish_requested.into()).await; self.schedule_publish_timeout(handle); diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 5aca39b5c..9f215a2d9 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -121,17 +121,16 @@ impl Drop for LocalTrackInner { /// /// # Examples /// -/// Create options for publishing a track named "my_track" with end-to-end encryption disabled: +/// Create options for publishing a track named "my_track": +/// /// ``` /// # use livekit_datatrack::api::DataTrackOptions; -/// let options = DataTrackOptions::new("my_track") -/// .disable_e2ee(true); // Set additional options as needed +/// let options = DataTrackOptions::new("my_track"); /// ``` /// #[derive(Clone, Debug)] pub struct DataTrackOptions { pub(crate) name: String, - pub(crate) disable_e2ee: bool, } impl DataTrackOptions { @@ -144,16 +143,7 @@ impl DataTrackOptions { /// - Must be unique per publisher /// pub fn new(name: impl Into) -> Self { - Self { name: name.into(), disable_e2ee: false } - } - - /// Disable end-to-end encryption. - /// - /// By default, room settings are used. - /// - pub fn disable_e2ee(mut self, disabled: bool) -> Self { - self.disable_e2ee = disabled; - self + Self { name: name.into() } } } diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 8c171f94c..5d51abf86 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -237,7 +237,7 @@ impl LocalParticipant { /// /// # Examples /// - /// Publish a track named "my_track" using default options: + /// Publish a track named "my_track": /// /// ``` /// # use livekit::prelude::*; @@ -250,22 +250,6 @@ impl LocalParticipant { /// # } /// ``` /// - /// Publish a track named "my_track" with custom options: - /// - /// ``` - /// # use livekit::prelude::*; - /// # async fn with_room(room: Room) -> Result<(), PublishError> { - /// let options = DataTrackOptions::new("my_track") - /// .disable_e2ee(true); // Disable end-to-end encryption for this track - /// - /// let track = room - /// .local_participant() - /// .publish_data_track(options) - /// .await?; - /// # Ok(()) - /// # } - /// ``` - /// pub async fn publish_data_track( &self, options: impl Into, From 0adda395d3f50ce854cb18b2793320055ebe5593 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:07:16 +1100 Subject: [PATCH 196/232] Add README --- examples/basic_data_track/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 examples/basic_data_track/README.md diff --git a/examples/basic_data_track/README.md b/examples/basic_data_track/README.md new file mode 100644 index 000000000..537d029ea --- /dev/null +++ b/examples/basic_data_track/README.md @@ -0,0 +1,21 @@ +# Basic Data Track + +Simple example of publishing and subscribing to a data track. + +## Usage + +1. Run the publisher: + +```sh +export LIVEKIT_URL="..." +export LIVEKIT_TOKEN="" +cargo run --bin publisher +``` + +2. In a second terminal, run the subscriber: + +```sh +export LIVEKIT_URL="..." +export LIVEKIT_TOKEN="" +cargo run --bin subscriber +``` From 52d8542b05d5897b621e5f48250950f844c59f4f Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:32:53 +1100 Subject: [PATCH 197/232] Update test for E2EE --- livekit/tests/data_track_test.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 7306807bd..24571615e 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -181,10 +181,8 @@ async fn test_publish_duplicate_name() -> Result<()> { } #[cfg(feature = "__lk-e2e-test")] -#[test_case(false; "use_room_settings")] -#[test_case(true; "disabled_on_track")] #[test_log::test(tokio::test)] -async fn test_e2ee(disable_on_track: bool) -> Result<()> { +async fn test_e2ee() -> Result<()> { use livekit::e2ee::{ key_provider::{KeyProvider, KeyProviderOptions}, EncryptionType, @@ -216,9 +214,8 @@ async fn test_e2ee(disable_on_track: bool) -> Result<()> { sub_room.e2ee_manager().set_enabled(true); let publish = async move { - let options = DataTrackOptions::new("my_track").disable_e2ee(disable_on_track); - let track = pub_room.local_participant().publish_data_track(options).await?; - assert!(track.info().uses_e2ee() || disable_on_track); + let track = pub_room.local_participant().publish_data_track("my_track").await?; + assert!(track.info().uses_e2ee()); for index in 0..5 { track.try_push(vec![index as u8; 196_608].into())?; @@ -239,7 +236,7 @@ async fn test_e2ee(disable_on_track: bool) -> Result<()> { } .await?; - assert!(track.info().uses_e2ee() || disable_on_track); + assert!(track.info().uses_e2ee()); let mut subscription = track.subscribe().await?; while let Some(frame) = subscription.next().await { From 272c89aa09dfc4e6a9de0d6a8f96c7cce45b6284 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:19:06 +1100 Subject: [PATCH 198/232] Fix manager logic to allow resubscribe --- livekit-datatrack/src/remote/manager.rs | 104 ++++++++++++---------- livekit-datatrack/src/utils/handle_map.rs | 53 ----------- livekit-datatrack/src/utils/mod.rs | 4 - livekit/tests/data_track_test.rs | 53 +++++++++++ 4 files changed, 111 insertions(+), 103 deletions(-) delete mode 100644 livekit-datatrack/src/utils/handle_map.rs diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 3235d6049..952b3febb 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -20,7 +20,6 @@ use crate::{ api::{DataTrackFrame, DataTrackInfo, DataTrackSid, InternalError, SubscribeError}, e2ee::DecryptionProvider, packet::{Handle, Packet}, - utils::HandleMap, }; use anyhow::{anyhow, Context}; use bytes::Bytes; @@ -121,8 +120,13 @@ pub struct Manager { /// Mapping between track SID and descriptor. descriptors: HashMap, - /// Bidirectional mapping between track SID and subscriber handle. - sub_handles: HandleMap, + + /// Bidirectional mapping between subscriber handle and track SID. + /// + /// This is an index that allows track descriptors to be looked up + /// by subscriber handle in O(1) time—necessary for routing incoming packets. + /// + sub_handles: HashMap, } impl Manager { @@ -145,7 +149,7 @@ impl Manager { event_in_rx, event_out_tx, descriptors: HashMap::default(), - sub_handles: HandleMap::default(), + sub_handles: HashMap::default(), }; let event_out = ReceiverStream::new(event_out_rx); @@ -213,7 +217,7 @@ impl Manager { info: info.clone(), publisher_identity: publisher_identity.clone(), published_tx, - state: DescriptorState::Available, + subscription: SubscriptionState::None, }; self.descriptors.insert(descriptor.info.sid.clone(), descriptor); @@ -227,13 +231,14 @@ impl Manager { } fn handle_track_unpublished(&mut self, sid: DataTrackSid) { - self.sub_handles.remove(&sid); let Some(descriptor) = self.descriptors.remove(&sid) else { log::error!("Unknown track {}", sid); return; }; + if let SubscriptionState::Active { sub_handle, .. } = descriptor.subscription { + self.sub_handles.remove(&sub_handle); + }; _ = descriptor.published_tx.send(false); - // TODO: this should end the track task } async fn handle_subscribe(&mut self, event: SubscribeEvent) { @@ -243,19 +248,20 @@ impl Manager { _ = event.result_tx.send(Err(error)); return; }; - match &mut descriptor.state { - DescriptorState::Available => { + match &mut descriptor.subscription { + SubscriptionState::None => { let update_event = SubscriptionUpdatedEvent { sid: event.sid.clone(), subscribe: true }; _ = self.event_out_tx.send(update_event.into()).await; - descriptor.state = - DescriptorState::PendingSubscriberHandle { result_txs: vec![event.result_tx] }; + descriptor.subscription = SubscriptionState::Pending { + result_txs: vec![event.result_tx], + }; // TODO: schedule timeout internally } - DescriptorState::PendingSubscriberHandle { result_txs } => { + SubscriptionState::Pending { result_txs } => { result_txs.push(event.result_tx); } - DescriptorState::Subscribed { frame_tx, .. } => { + SubscriptionState::Active { frame_tx, .. } => { let frame_rx = frame_tx.subscribe(); _ = event.result_tx.send(Ok(frame_rx)) } @@ -268,21 +274,21 @@ impl Manager { } } - fn register_subscriber_handle(&mut self, handle: Handle, sid: DataTrackSid) { + fn register_subscriber_handle(&mut self, sub_handle: Handle, sid: DataTrackSid) { let Some(descriptor) = self.descriptors.get_mut(&sid) else { log::warn!("Unknown track: {}", sid); return; }; - let result_txs = match &mut descriptor.state { - DescriptorState::Available => { + let result_txs = match &mut descriptor.subscription { + SubscriptionState::Pending { result_txs } => mem::take(result_txs), + SubscriptionState::None => { log::warn!("No subscription"); return; } - DescriptorState::Subscribed { .. } => { - log::warn!("Handle reassignment not implemented"); + SubscriptionState::Active { .. } => { + log::warn!("Cannot change existing handle"); return; } - DescriptorState::PendingSubscriberHandle { result_txs } => mem::take(result_txs), }; let (packet_tx, packet_rx) = mpsc::channel(4); // TODO: tune @@ -311,8 +317,9 @@ impl Manager { }; let task_handle = livekit_runtime::spawn(track_task.run()); - descriptor.state = DescriptorState::Subscribed { packet_tx, frame_tx, task_handle }; - self.sub_handles.insert(handle, sid); + descriptor.subscription = + SubscriptionState::Active { sub_handle, packet_tx, frame_tx, task_handle }; + self.sub_handles.insert(sub_handle, sid); for result_tx in result_txs { _ = result_tx.send(Ok(frame_rx.resubscribe())); @@ -327,16 +334,16 @@ impl Manager { return; } }; - let Some(sid) = self.sub_handles.get_sid(packet.header.track_handle) else { + let Some(sid) = self.sub_handles.get(&packet.header.track_handle) else { log::warn!("Unknown subscriber handle {}", packet.header.track_handle); return; }; let Some(descriptor) = self.descriptors.get(sid) else { - log::warn!("Missing descriptor"); + log::warn!("Missing descriptor for track {}", sid); return; }; - let DescriptorState::Subscribed { packet_tx, .. } = &descriptor.state else { - log::warn!("Received packet for track {} without subscription", descriptor.info.sid); + let SubscriptionState::Active { packet_tx, .. } = &descriptor.subscription else { + log::warn!("Received packet for track {} without subscription", sid); return; }; _ = packet_tx @@ -345,15 +352,17 @@ impl Manager { } async fn handle_unsubscribe(&mut self, event: UnsubscribeEvent) { - let Some(descriptor) = self.descriptors.remove(&event.sid) else { + let Some(descriptor) = self.descriptors.get_mut(&event.sid) else { return; }; - _ = descriptor.published_tx.send(false); - let DescriptorState::Subscribed { .. } = &descriptor.state else { + let SubscriptionState::Active { sub_handle, .. } = descriptor.subscription else { log::warn!("Unexpected state"); return; }; + descriptor.subscription = SubscriptionState::None; + self.sub_handles.remove(&sub_handle); + let event = SubscriptionUpdatedEvent { sid: event.sid, subscribe: false }; _ = self.event_out_tx.send(event.into()).await; } @@ -362,25 +371,41 @@ impl Manager { async fn shutdown(self) { for (_, descriptor) in self.descriptors { _ = descriptor.published_tx.send(false); - match descriptor.state { - DescriptorState::Available => {} - DescriptorState::PendingSubscriberHandle { result_txs } => { + match descriptor.subscription { + SubscriptionState::None => {} + SubscriptionState::Pending { result_txs } => { for result_tx in result_txs { _ = result_tx.send(Err(SubscribeError::Disconnected)); } } - DescriptorState::Subscribed { task_handle, .. } => task_handle.await, + SubscriptionState::Active { task_handle, .. } => task_handle.await, } } } } +/// Information and state for a remote data track. #[derive(Debug)] struct Descriptor { info: Arc, publisher_identity: Arc, published_tx: watch::Sender, - state: DescriptorState, + subscription: SubscriptionState, +} + +#[derive(Debug)] +enum SubscriptionState { + /// Track is not subscribed to. + None, + /// Track is being subscribed to, waiting for subscriber handle. + Pending { result_txs: Vec> }, + /// Track has an active subscription. + Active { + sub_handle: Handle, + packet_tx: mpsc::Sender, + frame_tx: broadcast::Sender, + task_handle: livekit_runtime::JoinHandle<()>, + }, } /// Task for an individual data track with an active subscription. @@ -428,19 +453,6 @@ impl TrackTask { } } -#[derive(Debug)] -enum DescriptorState { - Available, - PendingSubscriberHandle { - result_txs: Vec>, - }, - Subscribed { - packet_tx: mpsc::Sender, - frame_tx: broadcast::Sender, - task_handle: livekit_runtime::JoinHandle<()>, - }, -} - /// Channel for sending [`InputEvent`]s to [`Manager`]. #[derive(Debug, Clone)] pub struct ManagerInput { diff --git a/livekit-datatrack/src/utils/handle_map.rs b/livekit-datatrack/src/utils/handle_map.rs deleted file mode 100644 index fa02e12c3..000000000 --- a/livekit-datatrack/src/utils/handle_map.rs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2025 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 crate::{api::DataTrackSid, packet::Handle}; -use std::collections::HashMap; - -/// Map between track handle and SID. -/// -/// All operations are O(1). -/// -#[derive(Debug, Default)] -pub struct HandleMap { - sid_to_handle: HashMap, - handle_to_sid: HashMap, -} - -impl HandleMap { - /// Insert the given mapping between track handle and SID. - /// - /// Returns a Boolean indicating whether the entry was inserted. - /// Insertion will fail if the mapping already exists in either direction. - /// - pub fn insert(&mut self, handle: Handle, sid: DataTrackSid) -> bool { - if self.sid_to_handle.contains_key(&sid) || self.handle_to_sid.contains_key(&handle) { - return false; - } - self.sid_to_handle.insert(sid.clone(), handle); - self.handle_to_sid.insert(handle, sid); - true - } - - /// Get the SID associated with the given handle. - pub fn get_sid(&self, handle: Handle) -> Option<&DataTrackSid> { - self.handle_to_sid.get(&handle) - } - - /// Remove the mapping with the given SID. - pub fn remove(&mut self, sid: &DataTrackSid) { - let Some(handle) = self.sid_to_handle.remove(sid) else { return }; - self.handle_to_sid.remove(&handle); - } -} diff --git a/livekit-datatrack/src/utils/mod.rs b/livekit-datatrack/src/utils/mod.rs index bbaa84889..de7957d22 100644 --- a/livekit-datatrack/src/utils/mod.rs +++ b/livekit-datatrack/src/utils/mod.rs @@ -15,11 +15,7 @@ /// Utilities for working with [`Bytes::bytes`]. mod bytes; -/// Map between track handle and SID. -mod handle_map; - mod counter; pub use bytes::*; pub use counter::*; -pub use handle_map::*; diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 24571615e..b8f312e3b 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -299,3 +299,56 @@ async fn test_published_state() -> Result<()> { timeout(Duration::from_secs(5), async { try_join!(publish, subscribe) }).await??; Ok(()) } + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_resubscribe() -> Result<()> { + const ITERATIONS: usize = 10; + + let mut rooms = test_rooms(2).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); + + let publish = async move { + let track = pub_room.local_participant().publish_data_track("my_track").await.unwrap(); + loop { + _ = track.try_push(vec![0xFA; 64].into()); + time::sleep(Duration::from_millis(50)).await; + } + }; + + let subscribe = async move { + let track = async move { + while let Some(event) = sub_room_event_rx.recv().await { + let RoomEvent::RemoteDataTrackPublished(track) = event else { + continue; + }; + return Ok(track); + } + Err(anyhow!("No track published")) + } + .await + .unwrap(); + + let mut successful_subscriptions = 0; + for _ in 0..ITERATIONS { + let mut stream = track.subscribe().await.unwrap(); + while let Some(frame) = stream.next().await { + // Ensure we can at least get one frame. + assert!(!frame.payload().is_empty()); + successful_subscriptions += 1; + break; + } + std::mem::drop(stream); + time::sleep(Duration::from_millis(50)).await; + } + assert_eq!(successful_subscriptions, ITERATIONS); + }; + + let _ = timeout(Duration::from_secs(5), async { + tokio::select! { _ = publish => (), _ = subscribe => () }; + }) + .await?; + Ok(()) +} \ No newline at end of file From 6b065c96a6b64d219c758e6be15f2c6457b696f4 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:23:04 +1100 Subject: [PATCH 199/232] No deref impl Allow ID not to be string backed into the future. --- livekit-datatrack/src/track.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/livekit-datatrack/src/track.rs b/livekit-datatrack/src/track.rs index c9a4ae9e8..33dc34893 100644 --- a/livekit-datatrack/src/track.rs +++ b/livekit-datatrack/src/track.rs @@ -14,7 +14,7 @@ use crate::packet::Handle; use from_variants::FromVariants; -use std::{fmt::Display, marker::PhantomData, ops::Deref, sync::Arc}; +use std::{fmt::Display, marker::PhantomData, sync::Arc}; use tokio::sync::watch; use thiserror::Error; @@ -123,14 +123,6 @@ impl From for String { } } -impl Deref for DataTrackSid { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - impl Display for DataTrackSid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) From 380ea01f449976871b04cbdd790dfc7facd90a5e Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:05:22 +1100 Subject: [PATCH 200/232] Rename --- livekit-datatrack/src/local/pipeline.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/livekit-datatrack/src/local/pipeline.rs b/livekit-datatrack/src/local/pipeline.rs index f723bdb2d..ae74eed79 100644 --- a/livekit-datatrack/src/local/pipeline.rs +++ b/livekit-datatrack/src/local/pipeline.rs @@ -52,8 +52,8 @@ impl Pipeline { pub fn process_frame(&mut self, frame: DataTrackFrame) -> Result, PipelineError> { let frame = self.encrypt_if_needed(frame.into())?; - let frame = self.packetizer.packetize(frame)?; - Ok(frame) + let packets = self.packetizer.packetize(frame)?; + Ok(packets) } /// Encrypt the frame's payload if E2EE is enabled for this track. From 8349456b6763d5fc8cbeeda5770efd569a6dbda4 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:27:48 +1100 Subject: [PATCH 201/232] Add roundtrip test --- livekit-datatrack/src/packet/mod.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/livekit-datatrack/src/packet/mod.rs b/livekit-datatrack/src/packet/mod.rs index c0bcd76bd..0564fee29 100644 --- a/livekit-datatrack/src/packet/mod.rs +++ b/livekit-datatrack/src/packet/mod.rs @@ -31,7 +31,7 @@ pub struct Packet { pub payload: Bytes, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(fake::Dummy))] pub struct Header { pub marker: FrameMarker, @@ -88,3 +88,23 @@ pub(crate) mod consts { pub const EXT_MARKER_LEN: usize = 4; pub const EXT_TAG_PADDING: u16 = 0; } + +#[cfg(test)] +mod tests { + use super::Packet; + use fake::{Fake, Faker}; + + #[test] + fn test_roundtrip() { + let original: Packet = Faker.fake(); + + let header = original.header.clone(); + let payload = original.payload.clone(); + + let serialized = original.serialize(); + let deserialized = Packet::deserialize(serialized).unwrap(); + + assert_eq!(deserialized.header, header); + assert_eq!(deserialized.payload, payload); + } +} \ No newline at end of file From 31d7cc855adc3234ed863e8f37c7791cc89608d7 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:09:11 +1100 Subject: [PATCH 202/232] Doc --- livekit-datatrack/src/remote/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 952b3febb..f9998e181 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -121,7 +121,7 @@ pub struct Manager { /// Mapping between track SID and descriptor. descriptors: HashMap, - /// Bidirectional mapping between subscriber handle and track SID. + /// Mapping between subscriber handle and track SID. /// /// This is an index that allows track descriptors to be looked up /// by subscriber handle in O(1) time—necessary for routing incoming packets. From 02114d80dadbb8bb0e5cf82a3ffdb6aca31805ec Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:41:03 +1100 Subject: [PATCH 203/232] Start extension word count at zero This aligns with SFU expectations, spec will be updated --- livekit-datatrack/src/packet/deserialize.rs | 14 +++++++------- livekit-datatrack/src/packet/serialize.rs | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/livekit-datatrack/src/packet/deserialize.rs b/livekit-datatrack/src/packet/deserialize.rs index 6dd9a695b..adb01c9e5 100644 --- a/livekit-datatrack/src/packet/deserialize.rs +++ b/livekit-datatrack/src/packet/deserialize.rs @@ -81,7 +81,7 @@ impl Header { } let ext_words = raw.get_u16(); - let ext_len = 4 * ext_words as usize; + let ext_len = 4 * (ext_words + 1) as usize; if ext_len > raw.remaining() { Err(DeserializeError::HeaderOverrun)? } @@ -210,8 +210,8 @@ mod tests { let mut raw = valid_packet(); raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag - raw.put_u16(ext_words as u16); // 4 extension word - raw.put_bytes(0, ext_words * 4); // Padding + raw.put_u16(ext_words as u16); // Extension word + raw.put_bytes(0, (ext_words + 1) * 4); // Padding let packet = Packet::deserialize(raw.freeze()).unwrap(); assert_eq!(packet.payload.len(), 0); @@ -221,7 +221,7 @@ mod tests { fn test_ext_e2ee() { let mut raw = valid_packet(); raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag - raw.put_u16(5); // Extension words + raw.put_u16(4); // Extension words raw.put_u16(1); // ID 1 raw.put_u16(12); // Length 12 @@ -239,7 +239,7 @@ mod tests { fn test_ext_user_timestamp() { let mut raw = valid_packet(); raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag - raw.put_u16(3); // Extension words + raw.put_u16(2); // Extension words raw.put_u16(2); raw.put_u16(7); @@ -256,7 +256,7 @@ mod tests { fn test_ext_unknown() { let mut raw = valid_packet(); raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag - raw.put_u16(2); // Extension words + raw.put_u16(1); // Extension words raw.put_u16(8); // ID 8 (unknown) raw.put_bytes(0, 6); @@ -267,7 +267,7 @@ mod tests { fn test_ext_required_word_alignment() { let mut raw = valid_packet(); raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag - raw.put_u16(1); // Extension words + raw.put_u16(0); // Extension words raw.put_bytes(0, 3); // Padding, missing one byte assert!(Packet::deserialize(raw.freeze()).is_err()); diff --git a/livekit-datatrack/src/packet/serialize.rs b/livekit-datatrack/src/packet/serialize.rs index 3acd55ed1..bf186c422 100644 --- a/livekit-datatrack/src/packet/serialize.rs +++ b/livekit-datatrack/src/packet/serialize.rs @@ -117,7 +117,7 @@ impl Header { buf.put_u32(self.timestamp.as_ticks()); if metrics.ext_len > 0 { - buf.put_u16(metrics.ext_words as u16); + buf.put_u16((metrics.ext_words - 1) as u16); self.extensions.serialize_into(buf); buf.put_bytes(0, metrics.padding_len); } @@ -220,7 +220,7 @@ mod tests { assert_eq!(buf.get_u16(), 0x4422); // Sequence assert_eq!(buf.get_u16(), 0x4411); // Frame number assert_eq!(buf.get_u32(), 0x44221188); // Timestamp - assert_eq!(buf.get_u16(), 8); // Extension words + assert_eq!(buf.get_u16(), 7); // Extension words // E2EE extension assert_eq!(buf.get_u16(), 1); // ID 1, From 7c3e688283f706128067c83ac22043586d1e30d5 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:42:12 +1100 Subject: [PATCH 204/232] Style --- livekit-datatrack/src/packet/deserialize.rs | 2 +- livekit-datatrack/src/packet/serialize.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/livekit-datatrack/src/packet/deserialize.rs b/livekit-datatrack/src/packet/deserialize.rs index adb01c9e5..dfc5651cd 100644 --- a/livekit-datatrack/src/packet/deserialize.rs +++ b/livekit-datatrack/src/packet/deserialize.rs @@ -210,7 +210,7 @@ mod tests { let mut raw = valid_packet(); raw[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag - raw.put_u16(ext_words as u16); // Extension word + raw.put_u16(ext_words as u16); // Extension words raw.put_bytes(0, (ext_words + 1) * 4); // Padding let packet = Packet::deserialize(raw.freeze()).unwrap(); diff --git a/livekit-datatrack/src/packet/serialize.rs b/livekit-datatrack/src/packet/serialize.rs index bf186c422..5ca960dc5 100644 --- a/livekit-datatrack/src/packet/serialize.rs +++ b/livekit-datatrack/src/packet/serialize.rs @@ -98,10 +98,10 @@ impl Header { let mut initial = SUPPORTED_VERSION << VERSION_SHIFT; let marker = match self.marker { + FrameMarker::Single => FRAME_MARKER_SINGLE, + FrameMarker::Start => FRAME_MARKER_START, FrameMarker::Inter => FRAME_MARKER_INTER, FrameMarker::Final => FRAME_MARKER_FINAL, - FrameMarker::Start => FRAME_MARKER_START, - FrameMarker::Single => FRAME_MARKER_SINGLE, }; initial |= marker << FRAME_MARKER_SHIFT; From 29f232eded7f2bab1792e7e2a92763d666ed3a6a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:11:31 +1100 Subject: [PATCH 205/232] Test user timestamp, add convenience method --- livekit-datatrack/src/frame.rs | 24 +++++++++++++--- livekit/tests/data_track_test.rs | 49 ++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/livekit-datatrack/src/frame.rs b/livekit-datatrack/src/frame.rs index ed8195e16..86d52741e 100644 --- a/livekit-datatrack/src/frame.rs +++ b/livekit-datatrack/src/frame.rs @@ -14,6 +14,7 @@ use bytes::Bytes; use core::fmt; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// A frame published on a data track, consisting of a payload and optional metadata. /// @@ -45,6 +46,20 @@ impl DataTrackFrame { pub fn user_timestamp(&self) -> Option { self.user_timestamp } + + /// If the frame has a user timestamp, calculate how long has passed + /// relative to the current system time. + /// + /// If a timestamp is present, it is assumed it is a UNIX timestamp in milliseconds + /// (as can be set with [`Self::with_user_timestamp_now`] on the publisher side). + /// If the timestamp is invalid or not present, the result is none. + /// + pub fn duration_since_timestamp(&self) -> Option { + SystemTime::now() + .duration_since(UNIX_EPOCH + Duration::from_millis(self.user_timestamp?)) + .inspect_err(|err| log::error!("Failed to calculate duration: {err}")) + .ok() + } } impl DataTrackFrame { @@ -61,11 +76,12 @@ impl DataTrackFrame { /// Associates the current Unix timestamp (in milliseconds) with the frame. pub fn with_user_timestamp_now(mut self) -> Self { - let timestamp = std::time::SystemTime::now() + let timestamp = SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis() as u64; - self.user_timestamp = Some(timestamp); + .map(|d| d.as_millis() as u64) + .inspect_err(|err| log::error!("Failed to get system time: {err}")) + .ok(); + self.user_timestamp = timestamp; self } } diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index b8f312e3b..ecf7fd0ed 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -346,6 +346,55 @@ async fn test_resubscribe() -> Result<()> { assert_eq!(successful_subscriptions, ITERATIONS); }; + let _ = timeout(Duration::from_secs(5), async { + tokio::select! { _ = publish => (), _ = subscribe => () }; + }) + .await?; + Ok(()) +} + +#[cfg(feature = "__lk-e2e-test")] +#[test_log::test(tokio::test)] +async fn test_frame_with_user_timestamp() -> Result<()> { + + let mut rooms = test_rooms(2).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); + + let publish = async move { + let track = pub_room.local_participant().publish_data_track("my_track").await.unwrap(); + loop { + let frame = DataTrackFrame::new(vec![0xFA; 64]) + .with_user_timestamp_now(); + _ = track.try_push(frame); + time::sleep(Duration::from_millis(50)).await; + } + }; + + let subscribe = async move { + let track = async move { + while let Some(event) = sub_room_event_rx.recv().await { + let RoomEvent::RemoteDataTrackPublished(track) = event else { + continue; + }; + return Ok(track); + } + Err(anyhow!("No track published")) + } + .await + .unwrap(); + + let mut stream = track.subscribe().await.unwrap(); + while let Some(frame) = stream.next().await { + // Ensure we can at least get one frame. + assert!(!frame.payload().is_empty()); + let duration = frame.duration_since_timestamp().expect("Missing timestamp"); + assert!(duration.as_millis() < 1000); + break; + } + }; + let _ = timeout(Duration::from_secs(5), async { tokio::select! { _ = publish => (), _ = subscribe => () }; }) From eb5974742fd70816f388462ee600383fba4da0b0 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:16:36 +1100 Subject: [PATCH 206/232] Doc --- livekit-datatrack/src/local/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 9f215a2d9..39a8128b7 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -190,7 +190,7 @@ pub enum PublishError { Internal(#[from] InternalError), } -/// Frame could not be published to a data track. +/// Frame could not be pushed to a data track. #[derive(Debug, Error)] #[error("Failed to publish frame: {reason}")] pub struct PushFrameError { From d0779c19f85c997e868d3a73d3ab23fc7baf5008 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:21:18 +1100 Subject: [PATCH 207/232] Style --- examples/basic_data_track/src/subscriber.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/basic_data_track/src/subscriber.rs b/examples/basic_data_track/src/subscriber.rs index 78cd8cbb5..864960079 100644 --- a/examples/basic_data_track/src/subscriber.rs +++ b/examples/basic_data_track/src/subscriber.rs @@ -22,12 +22,12 @@ async fn main() -> Result<()> { /// Subscribe to the first data track published. async fn handle_first_publication(mut rx: UnboundedReceiver) -> Result<()> { + log::info!("Waiting for publication…"); while let Some(event) = rx.recv().await { - log::info!("Waiting for publication…"); - match event { - RoomEvent::RemoteDataTrackPublished(track) => subscribe(track).await?, - _ => continue, - } + let RoomEvent::RemoteDataTrackPublished(track) = event else { + continue; + }; + subscribe(track).await? } Ok(()) } From 6880a282dd15a948c11bbd652a4a40b4b23b598a Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:13:27 +1100 Subject: [PATCH 208/232] Drop reporting in depacketizer --- livekit-datatrack/src/remote/depacketizer.rs | 242 ++++++++++++++----- livekit-datatrack/src/remote/pipeline.rs | 12 +- 2 files changed, 197 insertions(+), 57 deletions(-) diff --git a/livekit-datatrack/src/remote/depacketizer.rs b/livekit-datatrack/src/remote/depacketizer.rs index 2a5aabe53..37b52f604 100644 --- a/livekit-datatrack/src/remote/depacketizer.rs +++ b/livekit-datatrack/src/remote/depacketizer.rs @@ -14,7 +14,8 @@ use crate::packet::{Extensions, FrameMarker, Packet}; use bytes::{Bytes, BytesMut}; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, fmt::Display}; +use thiserror::Error; /// Reassembles packets into frames. #[derive(Debug)] @@ -23,7 +24,8 @@ pub struct Depacketizer { partial: Option, } -/// Output of [`Depacketizer`]. +/// A frame that has been fully reassembled by [`Depacketizer`]. +#[derive(Debug)] pub struct DepacketizerFrame { pub payload: Bytes, pub extensions: Extensions, @@ -38,97 +40,100 @@ impl Depacketizer { Self { partial: None } } - /// Push a packet into the depacketizer, returning a complete frame if one is available. - pub fn push(&mut self, packet: Packet) -> Option { + /// Push a packet into the depacketizer. + pub fn push(&mut self, packet: Packet) -> DepacketizerPushResult { match packet.header.marker { - FrameMarker::Single => self.frame_from_single(packet).into(), - FrameMarker::Start => { - self.begin_partial(packet); - None - } - FrameMarker::Inter => { - self.push_to_partial(packet); - None - } - FrameMarker::Final => { - self.push_to_partial(packet); - self.finalize_partial() - } + FrameMarker::Single => self.frame_from_single(packet), + FrameMarker::Start => self.begin_partial(packet), + FrameMarker::Inter | FrameMarker::Final => self.push_to_partial(packet), } } - fn frame_from_single(&mut self, packet: Packet) -> DepacketizerFrame { + fn frame_from_single(&mut self, packet: Packet) -> DepacketizerPushResult { debug_assert!(packet.header.marker == FrameMarker::Single); - - if self.partial.is_some() { - log::trace!("Drop: interrupted"); - self.partial = None; + let mut result = DepacketizerPushResult::default(); + if let Some(partial) = self.partial.take() { + result.drop_error = DepacketizerDropError { + frame_number: partial.frame_number, + reason: DepacketizerDropReason::Interrupted, + } + .into(); } - DepacketizerFrame { payload: packet.payload, extensions: packet.header.extensions } + result.frame = + DepacketizerFrame { payload: packet.payload, extensions: packet.header.extensions } + .into(); + result } /// Begin assembling a new packet. - fn begin_partial(&mut self, packet: Packet) { + fn begin_partial(&mut self, packet: Packet) -> DepacketizerPushResult { debug_assert!(packet.header.marker == FrameMarker::Start); - if self.partial.is_some() { - log::trace!("Drop: interrupted"); - self.partial = None; + let mut result = DepacketizerPushResult::default(); + + if let Some(partial) = self.partial.take() { + result.drop_error = DepacketizerDropError { + frame_number: partial.frame_number, + reason: DepacketizerDropReason::Interrupted, + } + .into(); } + let start_sequence = packet.header.sequence; let payload_len = packet.payload.len(); let partial = PartialFrame { frame_number: packet.header.frame_number, start_sequence, - end_sequence: None, extensions: packet.header.extensions, payloads: BTreeMap::from([(start_sequence, packet.payload)]), payload_len, }; self.partial = partial.into(); + + result } /// Push to the existing partial frame. - fn push_to_partial(&mut self, packet: Packet) { + fn push_to_partial(&mut self, packet: Packet) -> DepacketizerPushResult { debug_assert!(matches!(packet.header.marker, FrameMarker::Inter | FrameMarker::Final)); let Some(mut partial) = self.partial.take() else { - log::trace!("Drop: unknown frame"); - return; + return DepacketizerDropError { + frame_number: packet.header.frame_number, + reason: DepacketizerDropReason::UnknownFrame, + } + .into(); }; if packet.header.frame_number != partial.frame_number { - log::trace!("Drop: interrupted"); - return; + return DepacketizerDropError { + frame_number: partial.frame_number, + reason: DepacketizerDropReason::Interrupted, + } + .into(); } if partial.payloads.len() == Self::MAX_BUFFER_PACKETS { - log::trace!("Drop: buffer full"); - return; + return DepacketizerDropError { + frame_number: partial.frame_number, + reason: DepacketizerDropReason::BufferFull, + } + .into(); } partial.payload_len += packet.payload.len(); partial.payloads.insert(packet.header.sequence, packet.payload); if packet.header.marker == FrameMarker::Final { - partial.end_sequence = packet.header.sequence.into(); + return Self::finalize(partial, packet.header.sequence); } self.partial = Some(partial); + DepacketizerPushResult::default() } - /// If there is a partial frame and it has an end sequence set, - /// return a complete frame from it. - /// - fn finalize_partial(&mut self) -> Option { - let Some(mut partial) = self.partial.take() else { - log::trace!("Drop: unknown frame"); - return None; - }; - let Some(end_sequence) = partial.end_sequence else { - log::trace!("Drop: no end sequence"); - return None; - }; - + /// Try to reassemble the complete frame. + fn finalize(mut partial: PartialFrame, end_sequence: u16) -> DepacketizerPushResult { + let received = partial.payloads.len() as u16; let mut sequence = partial.start_sequence; let mut payload = BytesMut::with_capacity(partial.payload_len); @@ -143,18 +148,24 @@ impl Depacketizer { return DepacketizerFrame { payload: payload.freeze(), extensions: partial.extensions } .into(); } - None + DepacketizerDropError { + frame_number: partial.frame_number, + reason: DepacketizerDropReason::Incomplete { + received, + expected: end_sequence - partial.start_sequence + 1, + }, + } + .into() } } +/// Frame being assembled as packets are received. #[derive(Debug)] struct PartialFrame { /// Frame number from the start packet. frame_number: u16, /// Sequence of the start packet. start_sequence: u16, - /// End sequence, if final packet has been received. - end_sequence: Option, /// Extensions from the start packet. extensions: Extensions, /// Mapping between sequence number and packet payload. @@ -163,6 +174,69 @@ struct PartialFrame { payload_len: usize, } +/// Result from a call to [`Depacketizer::push`]. +/// +/// The reason this type is used instead of [`core::result::Result`] is due to the fact a single +/// call to push can result in both a complete frame being delivered and a previous +/// frame being dropped. +/// +#[derive(Debug, Default)] +pub struct DepacketizerPushResult { + pub frame: Option, + pub drop_error: Option, +} + +impl From for DepacketizerPushResult { + fn from(frame: DepacketizerFrame) -> Self { + Self { frame: frame.into(), ..Default::default() } + } +} + +impl From for DepacketizerPushResult { + fn from(drop_event: DepacketizerDropError) -> Self { + Self { drop_error: drop_event.into(), ..Default::default() } + } +} + +/// An error indicating a frame was dropped. +#[derive(Debug, Error)] +#[error("Frame {frame_number} dropped: {reason}")] +pub struct DepacketizerDropError { + frame_number: u16, + reason: DepacketizerDropReason, +} + +/// Reason why a frame was dropped. +#[derive(Debug)] +pub enum DepacketizerDropReason { + /// Interrupted by the start of a new frame. + Interrupted, + /// Initial packet was never received. + UnknownFrame, + /// Reorder buffer is full. + BufferFull, + /// Not all packets received before final packet. + Incomplete { + /// Number of packets received. + received: u16, + /// Number of packets expected. + expected: u16, + }, +} + +impl Display for DepacketizerDropReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DepacketizerDropReason::Interrupted => write!(f, "interrupted"), + DepacketizerDropReason::UnknownFrame => write!(f, "unknown frame"), + DepacketizerDropReason::BufferFull => write!(f, "buffer full"), + DepacketizerDropReason::Incomplete { received, expected } => { + write!(f, "incomplete ({}/{})", received, expected) + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -176,7 +250,11 @@ mod tests { let mut packet: Packet = Faker.fake(); packet.header.marker = FrameMarker::Single; - let frame = depacketizer.push(packet.clone()).unwrap(); + let result = depacketizer.push(packet.clone()); + + assert!(result.drop_error.is_none()); + let frame = result.frame.unwrap(); + assert_eq!(frame.payload, packet.payload); assert_eq!(frame.extensions, packet.header.extensions); } @@ -190,22 +268,74 @@ mod tests { let mut packet: Packet = Faker.fake(); packet.header.marker = FrameMarker::Start; - assert!(depacketizer.push(packet.clone()).is_none()); + let result = depacketizer.push(packet.clone()); + assert!(result.frame.is_none() && result.drop_error.is_none()); for _ in 0..inter_packets { packet.header.marker = FrameMarker::Inter; packet.header.sequence += 1; - assert!(depacketizer.push(packet.clone()).is_none()); + + let result = depacketizer.push(packet.clone()); + assert!(result.frame.is_none() && result.drop_error.is_none()); } packet.header.marker = FrameMarker::Final; packet.header.sequence += 1; - let frame = depacketizer.push(packet.clone()).unwrap(); + let result = depacketizer.push(packet.clone()); + + assert!(result.drop_error.is_none()); + let frame = result.frame.unwrap(); + assert_eq!(frame.extensions, packet.header.extensions); assert_eq!(frame.payload.len(), packet.payload.len() * (inter_packets + 2)); } + #[test] + fn test_interrupted() { + let mut depacketizer = Depacketizer::new(); + + let mut packet: Packet = Faker.fake(); + packet.header.marker = FrameMarker::Start; + + let result = depacketizer.push(packet.clone()); + assert!(result.frame.is_none() && result.drop_error.is_none()); + + let first_frame_number = packet.header.frame_number; + packet.header.frame_number += 1; // Next frame + + let result = depacketizer.push(packet); + assert!(result.frame.is_none()); + + let drop = result.drop_error.unwrap(); + assert_eq!(drop.frame_number, first_frame_number); + assert!(matches!(drop.reason, DepacketizerDropReason::Interrupted)); + } + + #[test] + fn test_incomplete() { + let mut depacketizer = Depacketizer::new(); + + let mut packet: Packet = Faker.fake(); + let frame_number = packet.header.frame_number; + packet.header.marker = FrameMarker::Start; + + depacketizer.push(packet.clone()); + + packet.header.sequence += 3; + packet.header.marker = FrameMarker::Final; + + let result = depacketizer.push(packet); + assert!(result.frame.is_none()); + + let drop = result.drop_error.unwrap(); + assert_eq!(drop.frame_number, frame_number); + assert!(matches!( + drop.reason, + DepacketizerDropReason::Incomplete { received: 2, expected: 4 } + )); + } + impl fake::Dummy for Packet { fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { let payload_len = rng.random_range(0..=1500); diff --git a/livekit-datatrack/src/remote/pipeline.rs b/livekit-datatrack/src/remote/pipeline.rs index cc15d9b73..8e6d443f0 100644 --- a/livekit-datatrack/src/remote/pipeline.rs +++ b/livekit-datatrack/src/remote/pipeline.rs @@ -47,11 +47,21 @@ impl Pipeline { } pub fn process_packet(&mut self, packet: Packet) -> Option { - let Some(frame) = self.depacketizer.push(packet) else { return None }; + let Some(frame) = self.depacketize(packet) else { return None }; let Some(frame) = self.decrypt_if_needed(frame) else { return None }; Some(frame.into()) } + /// Depacketize the given frame, log if a drop occurs. + fn depacketize(&mut self, packet: Packet) -> Option { + let result = self.depacketizer.push(packet); + if let Some(drop) = result.drop_error { + // In a future version, use this to maintain drop statistics. + log::debug!("{}", drop); + }; + result.frame + } + /// Decrypt the frame's payload if E2EE is enabled for this track. fn decrypt_if_needed(&self, mut frame: DepacketizerFrame) -> Option { let Some(decryption) = &self.e2ee_provider else { return frame.into() }; From 6b4f4e468ab8b9e808cb28218bca24d93e123bba Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:14:41 +1100 Subject: [PATCH 209/232] Add note --- livekit-datatrack/src/e2ee.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/livekit-datatrack/src/e2ee.rs b/livekit-datatrack/src/e2ee.rs index 1f51e43cc..0e21a607a 100644 --- a/livekit-datatrack/src/e2ee.rs +++ b/livekit-datatrack/src/e2ee.rs @@ -16,6 +16,9 @@ use bytes::Bytes; use core::fmt::Debug; use thiserror::Error; +// TODO: If a core module for end-to-end encryption is created in the future +// (livekit-e2ee), these traits should be moved to there. + /// Encrypted payload and metadata required for decryption. pub struct EncryptedPayload { pub payload: Bytes, From 2c6e14903f7419141bfeb0b92438726d2c93a084 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:20:00 +1100 Subject: [PATCH 210/232] Latency in basic example --- examples/basic_data_track/src/publisher.rs | 5 ++++- examples/basic_data_track/src/subscriber.rs | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/basic_data_track/src/publisher.rs b/examples/basic_data_track/src/publisher.rs index f152efc28..e0600947e 100644 --- a/examples/basic_data_track/src/publisher.rs +++ b/examples/basic_data_track/src/publisher.rs @@ -29,7 +29,10 @@ async fn read_sensor() -> Vec { async fn push_frames(track: LocalDataTrack) { loop { log::info!("Pushing frame"); - let frame = read_sensor().await.into(); + + let frame = DataTrackFrame::new(read_sensor().await) + .with_user_timestamp_now(); + track.try_push(frame).inspect_err(|err| println!("Failed to push frame: {}", err)).ok(); time::sleep(Duration::from_millis(500)).await } diff --git a/examples/basic_data_track/src/subscriber.rs b/examples/basic_data_track/src/subscriber.rs index 864960079..fc489133a 100644 --- a/examples/basic_data_track/src/subscriber.rs +++ b/examples/basic_data_track/src/subscriber.rs @@ -42,6 +42,10 @@ async fn subscribe(track: RemoteDataTrack) -> Result<()> { let mut stream = track.subscribe().await?; while let Some(frame) = stream.next().await { log::info!("Received frame ({} bytes)", frame.payload().len()); + + if let Some(duration) = frame.duration_since_timestamp() { + log::info!("Latency: {:?}", duration); + } } log::info!("Unsubscribed"); Ok(()) From 6bbf620df9c3b94fc76ec20762da306474bb3926 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:59:51 +1100 Subject: [PATCH 211/232] Improve timeout handling (local) --- livekit-datatrack/src/local/manager.rs | 78 ++++++++++++++++++-------- 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index e2a244d8c..655b2fe96 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -28,7 +28,6 @@ use from_variants::FromVariants; use futures_core::Stream; use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::sync::{mpsc, oneshot, watch}; -use tokio::time; use tokio_stream::wrappers::ReceiverStream; /// An external event handled by [`Manager`]. @@ -36,6 +35,7 @@ use tokio_stream::wrappers::ReceiverStream; pub enum InputEvent { Publish(PublishEvent), PublishResult(PublishResultEvent), + PublishCancelled(PublishCancelledEvent), Unpublish(UnpublishEvent), /// Shutdown the manager and all associated tracks. Shutdown, @@ -96,9 +96,9 @@ pub struct PublishEvent { result_tx: oneshot::Sender>, } -/// Request to publish a data track timed-out. +/// Request to publish a data track was cancelled. #[derive(Debug)] -pub struct PublishTimeoutEvent { +pub struct PublishCancelledEvent { /// Publisher handle of the pending publication. handle: Handle, } @@ -161,6 +161,7 @@ impl Manager { match event { InputEvent::Publish(event) => self.handle_publish(event).await, InputEvent::PublishResult(event) => self.handle_publish_result(event).await, + InputEvent::PublishCancelled(event) => self.handle_publish_cancelled(event).await, InputEvent::Unpublish(event) => self.handle_unpublished(event).await, InputEvent::Shutdown => break, } @@ -181,7 +182,16 @@ impl Manager { ))); return; } - self.descriptors.insert(handle, Descriptor::Pending(event.result_tx)); + + let (result_tx, result_rx) = oneshot::channel(); + self.descriptors.insert(handle, Descriptor::Pending(result_tx)); + + livekit_runtime::spawn(Self::forward_publish_result( + handle, + result_rx, + event.result_tx, + self.event_in_tx.downgrade(), + )); let publish_requested = PublishRequestEvent { handle, @@ -189,18 +199,36 @@ impl Manager { uses_e2ee: self.encryption_provider.is_some(), }; _ = self.event_out_tx.send(publish_requested.into()).await; - self.schedule_publish_timeout(handle); } - fn schedule_publish_timeout(&self, handle: Handle) { - let event_in_tx = self.event_in_tx.downgrade(); - let emit_timeout = async move { - time::sleep(Self::PUBLISH_TIMEOUT).await; - let Some(tx) = event_in_tx.upgrade() else { return }; - let event = PublishResultEvent { handle, result: Err(PublishError::Timeout) }; - _ = tx.try_send(event.into()) - }; - livekit_runtime::spawn(emit_timeout); + /// Task that awaits a pending publish result. + /// + /// Forwards the result to the user, or notifies the manager if the receiver + /// is dropped (e.g., due to timeout) so it can remove the pending publication. + /// + async fn forward_publish_result( + handle: Handle, + result_rx: oneshot::Receiver>, + mut forward_result_tx: oneshot::Sender>, + event_in_tx: mpsc::WeakSender, + ) { + tokio::select! { + biased; + Ok(result) = result_rx => { + _ = forward_result_tx.send(result); + } + _ = forward_result_tx.closed() => { + let Some(tx) = event_in_tx.upgrade() else { return }; + let event = PublishCancelledEvent { handle }; + _ = tx.try_send(event.into()); + } + } + } + + async fn handle_publish_cancelled(&mut self, event: PublishCancelledEvent) { + if self.descriptors.remove(&event.handle).is_none() { + log::warn!("No descriptor for {}", event.handle); + } } async fn handle_publish_result(&mut self, event: PublishResultEvent) { @@ -281,9 +309,6 @@ impl Manager { } } } - - /// How long to wait for an SFU response for a track publication before timeout. - const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); } /// Task for an individual published data track. @@ -367,11 +392,20 @@ impl ManagerInput { let (result_tx, result_rx) = oneshot::channel(); let event = PublishEvent { options, result_tx }; - self.event_in_tx.try_send(event.into()).map_err(|_| PublishError::Disconnected)?; - let track = result_rx.await.map_err(|_| PublishError::Disconnected)??; + self.event_in_tx + .try_send(event.into()) + .map_err(|_| PublishError::Disconnected)?; + + let track = tokio::time::timeout(Self::PUBLISH_TIMEOUT, result_rx) + .await + .map_err(|_| PublishError::Timeout)? + .map_err(|_| PublishError::Disconnected)??; Ok(track) } + + /// How long to wait for before timeout. + const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); } #[cfg(test)] @@ -380,7 +414,7 @@ mod tests { use crate::{api::DataTrackSid, packet::Packet}; use fake::{Fake, Faker}; use futures_util::StreamExt; - use livekit_runtime::sleep; + use livekit_runtime::{timeout, sleep}; #[tokio::test] async fn test_task_shutdown() { @@ -390,7 +424,7 @@ mod tests { let join_handle = livekit_runtime::spawn(manager.run()); _ = input.send(InputEvent::Shutdown); - time::timeout(Duration::from_secs(1), join_handle).await.unwrap(); + timeout(Duration::from_secs(1), join_handle).await.unwrap(); } #[tokio::test] @@ -453,7 +487,7 @@ mod tests { } // Only reference to track dropped here (unpublish) }; - time::timeout(Duration::from_secs(1), async { tokio::join!(publish_track, handle_events) }) + timeout(Duration::from_secs(1), async { tokio::join!(publish_track, handle_events) }) .await .unwrap(); } From 09416eb076db86a04d3f46fc3bc9b7be41b6e082 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:43:32 +1100 Subject: [PATCH 212/232] Events in separate module (local) --- livekit-datatrack/src/lib.rs | 7 +- livekit-datatrack/src/local/events.rs | 99 ++++++++++++++++++++++++++ livekit-datatrack/src/local/manager.rs | 96 +++---------------------- livekit-datatrack/src/local/mod.rs | 3 +- livekit-datatrack/src/local/proto.rs | 29 ++++---- 5 files changed, 132 insertions(+), 102 deletions(-) create mode 100644 livekit-datatrack/src/local/events.rs diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index d43071130..9785ba5b2 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -44,8 +44,13 @@ pub mod api { /// Internal APIs for use within the LiveKit crate. pub mod internal { pub use crate::e2ee::*; + pub mod local { - pub use crate::local::{manager::*, proto::*}; + pub use crate::local::{ + events::*, + manager::*, + proto::*, + }; } pub mod remote { pub use crate::remote::{manager::*, proto::*}; diff --git a/livekit-datatrack/src/local/events.rs b/livekit-datatrack/src/local/events.rs new file mode 100644 index 000000000..3a7b699fd --- /dev/null +++ b/livekit-datatrack/src/local/events.rs @@ -0,0 +1,99 @@ +// Copyright 2025 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 crate::{ + api::{DataTrackInfo, DataTrackOptions, LocalDataTrack, PublishError}, + packet::Handle, +}; +use bytes::Bytes; +use from_variants::FromVariants; +use tokio::sync::oneshot; + +// MARK: - Input events + +/// An external event handled by [`super::manager::Manager`]. +#[derive(Debug, FromVariants)] +pub enum InputEvent { + Publish(PublishEvent), + PublishResult(PublishResultEvent), + PublishCancelled(PublishCancelledEvent), + Unpublish(UnpublishEvent), + /// Shutdown the manager and all associated tracks. + Shutdown, +} + +/// Request to publish a data track. +#[derive(Debug)] +pub struct PublishEvent { + /// Publish options. + pub(super) options: DataTrackOptions, + /// Async completion channel. + pub(super) result_tx: oneshot::Sender>, +} + + +/// Result of a publish request. +#[derive(Debug)] +pub struct PublishResultEvent { + /// Publisher handle of the track. + pub handle: Handle, + /// Outcome of the publish request. + pub result: Result, +} + +/// Request to publish a data track was cancelled. +#[derive(Debug)] +pub struct PublishCancelledEvent { + /// Publisher handle of the pending publication. + pub(super) handle: Handle, +} + +/// Track has been unpublished. +#[derive(Debug)] +pub struct UnpublishEvent { + /// Publisher handle of the track that was unpublished. + pub handle: Handle, + /// Whether the unpublish was initiated by the client. + pub client_initiated: bool, +} + +// MARK: - Output events + +/// An event produced by [`super::manager::Manager`] requiring external action. +#[derive(Debug, FromVariants)] +pub enum OutputEvent { + PublishRequest(PublishRequestEvent), + UnpublishRequest(UnpublishRequestEvent), + /// Serialized packets are ready to be sent over the transport. + PacketsAvailable(Vec), +} + +/// Local participant requested to publish a track. +#[derive(Debug)] +pub struct PublishRequestEvent { + pub handle: Handle, + pub name: String, + pub uses_e2ee: bool, +} + +/// Local participant unpublished a track. +/// +/// This can either occur explicitly through user action or implicitly when the last +/// reference to the track is dropped. +/// +#[derive(Debug)] +pub struct UnpublishRequestEvent { + /// Publisher handle of the track to unpublish. + pub handle: Handle, +} diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 655b2fe96..260041817 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -14,6 +14,7 @@ use super::{ pipeline::{Pipeline, PipelineOptions}, + events::{self, InputEvent, OutputEvent}, LocalTrackInner, }; use crate::{ @@ -23,86 +24,11 @@ use crate::{ packet::{self, Handle}, }; use anyhow::{anyhow, Context}; -use bytes::Bytes; -use from_variants::FromVariants; use futures_core::Stream; use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::sync::{mpsc, oneshot, watch}; use tokio_stream::wrappers::ReceiverStream; -/// An external event handled by [`Manager`]. -#[derive(Debug, FromVariants)] -pub enum InputEvent { - Publish(PublishEvent), - PublishResult(PublishResultEvent), - PublishCancelled(PublishCancelledEvent), - Unpublish(UnpublishEvent), - /// Shutdown the manager and all associated tracks. - Shutdown, -} - -/// An event produced by [`Manager`] requiring external action. -#[derive(Debug, FromVariants)] -pub enum OutputEvent { - PublishRequest(PublishRequestEvent), - UnpublishRequest(UnpublishRequestEvent), - /// Serialized packets are ready to be sent over the transport. - PacketsAvailable(Vec), -} - -/// Result of a publish request. -#[derive(Debug)] -pub struct PublishResultEvent { - /// Publisher handle of the track. - pub handle: Handle, - /// Outcome of the publish request. - pub result: Result, -} - -/// Track has been unpublished. -#[derive(Debug)] -pub struct UnpublishEvent { - /// Publisher handle of the track that was unpublished. - pub handle: Handle, - /// Whether the unpublish was initiated by the client. - pub client_initiated: bool, -} - -/// Local participant requested to publish a track. -#[derive(Debug)] -pub struct PublishRequestEvent { - pub handle: Handle, - pub name: String, - pub uses_e2ee: bool, -} - -/// Local participant unpublished a track. -/// -/// This can either occur explicitly through user action or implicitly when the last -/// reference to the track is dropped. -/// -#[derive(Debug)] -pub struct UnpublishRequestEvent { - /// Publisher handle of the track to unpublish. - pub handle: Handle, -} - -/// Request to publish a data track. -#[derive(Debug)] -pub struct PublishEvent { - /// Publish options. - options: DataTrackOptions, - /// Async completion channel. - result_tx: oneshot::Sender>, -} - -/// Request to publish a data track was cancelled. -#[derive(Debug)] -pub struct PublishCancelledEvent { - /// Publisher handle of the pending publication. - handle: Handle, -} - /// Options for creating a [`Manager`]. #[derive(Debug)] pub struct ManagerOptions { @@ -170,7 +96,7 @@ impl Manager { log::debug!("Task ended"); } - async fn handle_publish(&mut self, event: PublishEvent) { + async fn handle_publish(&mut self, event: events::PublishEvent) { let Some(handle) = self.handle_allocator.get() else { _ = event.result_tx.send(Err(PublishError::LimitReached)); return; @@ -193,7 +119,7 @@ impl Manager { self.event_in_tx.downgrade(), )); - let publish_requested = PublishRequestEvent { + let publish_requested = events::PublishRequestEvent { handle, name: event.options.name, uses_e2ee: self.encryption_provider.is_some(), @@ -219,19 +145,19 @@ impl Manager { } _ = forward_result_tx.closed() => { let Some(tx) = event_in_tx.upgrade() else { return }; - let event = PublishCancelledEvent { handle }; + let event = events::PublishCancelledEvent { handle }; _ = tx.try_send(event.into()); } } } - async fn handle_publish_cancelled(&mut self, event: PublishCancelledEvent) { + async fn handle_publish_cancelled(&mut self, event: events::PublishCancelledEvent) { if self.descriptors.remove(&event.handle).is_none() { log::warn!("No descriptor for {}", event.handle); } } - async fn handle_publish_result(&mut self, event: PublishResultEvent) { + async fn handle_publish_result(&mut self, event: events::PublishResultEvent) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { log::warn!("No descriptor for {}", event.handle); return; @@ -278,7 +204,7 @@ impl Manager { LocalDataTrack::new(info, inner) } - async fn handle_unpublished(&mut self, event: UnpublishEvent) { + async fn handle_unpublished(&mut self, event: events::UnpublishEvent) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { return; }; @@ -290,7 +216,7 @@ impl Manager { } if event.client_initiated { // Inform the SFU if client initiated. - let event = UnpublishRequestEvent { handle: event.handle }; + let event = events::UnpublishRequestEvent { handle: event.handle }; _ = self.event_out_tx.send(event.into()).await; } } @@ -335,7 +261,7 @@ impl TrackTask { } } - let event = UnpublishEvent { handle: self.info.pub_handle, client_initiated: true }; + let event = events::UnpublishEvent { handle: self.info.pub_handle, client_initiated: true }; _ = self.event_in_tx.send(event.into()).await; log::debug!("Track task ended: sid={}", self.info.sid); @@ -390,7 +316,7 @@ impl ManagerInput { options: DataTrackOptions, ) -> Result { let (result_tx, result_rx) = oneshot::channel(); - let event = PublishEvent { options, result_tx }; + let event = events::PublishEvent { options, result_tx }; self.event_in_tx .try_send(event.into()) @@ -457,7 +383,7 @@ mod tests { uses_e2ee: event.uses_e2ee, }; let input_event = - PublishResultEvent { handle: event.handle, result: Ok(info) }; + events::PublishResultEvent { handle: event.handle, result: Ok(info) }; _ = input.send(input_event.into()); } OutputEvent::PacketsAvailable(packets) => { diff --git a/livekit-datatrack/src/local/mod.rs b/livekit-datatrack/src/local/mod.rs index 39a8128b7..667290241 100644 --- a/livekit-datatrack/src/local/mod.rs +++ b/livekit-datatrack/src/local/mod.rs @@ -20,8 +20,9 @@ use std::{fmt, marker::PhantomData, sync::Arc}; use thiserror::Error; use tokio::sync::{mpsc, watch}; -pub(crate) mod manager; pub(crate) mod proto; +pub(crate) mod events; +pub(crate) mod manager; mod packetizer; mod pipeline; diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index a1bd2423d..6b65dc479 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::manager::{PublishRequestEvent, UnpublishEvent, UnpublishRequestEvent}; +use super::events; use crate::{ api::{DataTrackInfo, DataTrackSid, InternalError, PublishError}, - local::manager::PublishResultEvent, packet::Handle, }; use anyhow::{anyhow, Context}; @@ -24,23 +23,23 @@ use std::mem; // MARK: - Output event -> protocol -impl From for proto::PublishDataTrackRequest { - fn from(event: PublishRequestEvent) -> Self { +impl From for proto::PublishDataTrackRequest { + fn from(event: events::PublishRequestEvent) -> 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 } } } -impl From for proto::UnpublishDataTrackRequest { - fn from(event: UnpublishRequestEvent) -> Self { +impl From for proto::UnpublishDataTrackRequest { + fn from(event: events::UnpublishRequestEvent) -> Self { Self { pub_handle: event.handle.into() } } } // MARK: - Protocol -> input event -impl TryFrom for PublishResultEvent { +impl TryFrom for events::PublishResultEvent { type Error = InternalError; fn try_from(msg: proto::PublishDataTrackResponse) -> Result { @@ -49,7 +48,7 @@ impl TryFrom for PublishResultEvent { } } -impl TryFrom for UnpublishEvent { +impl TryFrom for events::UnpublishEvent { type Error = InternalError; fn try_from(msg: proto::UnpublishDataTrackResponse) -> Result { @@ -76,7 +75,7 @@ impl TryFrom for DataTrackInfo { pub fn publish_result_from_request_response( msg: &proto::RequestResponse, -) -> Option { +) -> Option { use proto::request_response::{Reason, Request}; let Some(request) = &msg.request else { return None }; let Request::PublishDataTrack(request) = request else { return None }; @@ -88,16 +87,16 @@ pub fn publish_result_from_request_response( Reason::DuplicateName => PublishError::DuplicateName, _ => PublishError::Internal(anyhow!("SFU rejected: {}", msg.message).into()), }; - let event = PublishResultEvent { handle, result: Err(error) }; + let event = events::PublishResultEvent { handle, result: Err(error) }; Some(event) } pub fn publish_results_from_sync_state( msg: &mut proto::SyncState, -) -> Result, InternalError> { +) -> Result, InternalError> { mem::take(&mut msg.publish_data_tracks) .into_iter() - .map(TryInto::::try_into) + .map(TryInto::::try_into) .collect::, InternalError>>() } @@ -107,7 +106,7 @@ mod tests { #[test] fn test_from_publish_request_event() { - let event = PublishRequestEvent { + let event = events::PublishRequestEvent { handle: 1u32.try_into().unwrap(), name: "track".into(), uses_e2ee: true, @@ -120,7 +119,7 @@ mod tests { #[test] fn test_from_unpublish_request_event() { - let event = UnpublishRequestEvent { handle: 1u32.try_into().unwrap() }; + let event = events::UnpublishRequestEvent { handle: 1u32.try_into().unwrap() }; let request: proto::UnpublishDataTrackRequest = event.into(); assert_eq!(request.pub_handle, 1); } @@ -136,7 +135,7 @@ mod tests { } .into(), }; - let event: PublishResultEvent = response.try_into().unwrap(); + let event: events::PublishResultEvent = response.try_into().unwrap(); assert_eq!(event.handle, 1u32.try_into().unwrap()); let info = event.result.expect("Expected ok result"); From 21898f44c62a1518428ba4643dd68ad8d401798b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:17:00 +1100 Subject: [PATCH 213/232] Rename events for clarity (local) --- livekit-datatrack/src/local/events.rs | 50 ++++++++++++-------------- livekit-datatrack/src/local/manager.rs | 47 ++++++++++++------------ livekit-datatrack/src/local/proto.rs | 28 +++++++-------- livekit/src/rtc_engine/rtc_session.rs | 8 ++--- livekit/tests/data_track_test.rs | 7 +++- 5 files changed, 71 insertions(+), 69 deletions(-) diff --git a/livekit-datatrack/src/local/events.rs b/livekit-datatrack/src/local/events.rs index 3a7b699fd..d44ce685e 100644 --- a/livekit-datatrack/src/local/events.rs +++ b/livekit-datatrack/src/local/events.rs @@ -25,47 +25,47 @@ use tokio::sync::oneshot; /// An external event handled by [`super::manager::Manager`]. #[derive(Debug, FromVariants)] pub enum InputEvent { - Publish(PublishEvent), - PublishResult(PublishResultEvent), - PublishCancelled(PublishCancelledEvent), - Unpublish(UnpublishEvent), + PublishRequest(PublishRequest), + PublishCancelled(PublishCancelled), + SfuPublishResponse(SfuPublishResponse), + SfuUnpublishResponse(SfuUnpublishResponse), /// Shutdown the manager and all associated tracks. Shutdown, } -/// Request to publish a data track. +/// User requested to publish a track. #[derive(Debug)] -pub struct PublishEvent { +pub struct PublishRequest { /// Publish options. pub(super) options: DataTrackOptions, /// Async completion channel. pub(super) result_tx: oneshot::Sender>, } +/// User request to publish a track has been cancelled. +#[derive(Debug)] +pub struct PublishCancelled { + /// Publisher handle of the pending publication. + pub(super) handle: Handle, +} -/// Result of a publish request. +/// SFU responded to a request to publish a data track. #[derive(Debug)] -pub struct PublishResultEvent { +pub struct SfuPublishResponse { /// Publisher handle of the track. pub handle: Handle, /// Outcome of the publish request. pub result: Result, } -/// Request to publish a data track was cancelled. -#[derive(Debug)] -pub struct PublishCancelledEvent { - /// Publisher handle of the pending publication. - pub(super) handle: Handle, -} - -/// Track has been unpublished. +/// SFU notification that a track has been unpublished. #[derive(Debug)] -pub struct UnpublishEvent { +pub struct SfuUnpublishResponse { /// Publisher handle of the track that was unpublished. pub handle: Handle, /// Whether the unpublish was initiated by the client. pub client_initiated: bool, + // TODO: this should be made into a separate event } // MARK: - Output events @@ -73,27 +73,23 @@ pub struct UnpublishEvent { /// An event produced by [`super::manager::Manager`] requiring external action. #[derive(Debug, FromVariants)] pub enum OutputEvent { - PublishRequest(PublishRequestEvent), - UnpublishRequest(UnpublishRequestEvent), + SfuPublishRequest(SfuPublishRequest), + SfuUnpublishRequest(SfuUnpublishRequest), /// Serialized packets are ready to be sent over the transport. PacketsAvailable(Vec), } -/// Local participant requested to publish a track. +/// Request sent to the SFU to publish a track. #[derive(Debug)] -pub struct PublishRequestEvent { +pub struct SfuPublishRequest { pub handle: Handle, pub name: String, pub uses_e2ee: bool, } -/// Local participant unpublished a track. -/// -/// This can either occur explicitly through user action or implicitly when the last -/// reference to the track is dropped. -/// +/// Request sent to the SFU to unpublish a track. #[derive(Debug)] -pub struct UnpublishRequestEvent { +pub struct SfuUnpublishRequest { /// Publisher handle of the track to unpublish. pub handle: Handle, } diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 260041817..e549846c5 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -13,8 +13,8 @@ // limitations under the License. use super::{ + events::*, pipeline::{Pipeline, PipelineOptions}, - events::{self, InputEvent, OutputEvent}, LocalTrackInner, }; use crate::{ @@ -85,10 +85,14 @@ impl Manager { while let Some(event) = self.event_in_rx.recv().await { log::debug!("Input event: {:?}", event); match event { - InputEvent::Publish(event) => self.handle_publish(event).await, - InputEvent::PublishResult(event) => self.handle_publish_result(event).await, + InputEvent::PublishRequest(event) => self.handle_publish_request(event).await, InputEvent::PublishCancelled(event) => self.handle_publish_cancelled(event).await, - InputEvent::Unpublish(event) => self.handle_unpublished(event).await, + InputEvent::SfuPublishResponse(event) => { + self.handle_sfu_publish_response(event).await + } + InputEvent::SfuUnpublishResponse(event) => { + self.handle_sfu_unpublish_response(event).await + } InputEvent::Shutdown => break, } } @@ -96,7 +100,7 @@ impl Manager { log::debug!("Task ended"); } - async fn handle_publish(&mut self, event: events::PublishEvent) { + async fn handle_publish_request(&mut self, event: PublishRequest) { let Some(handle) = self.handle_allocator.get() else { _ = event.result_tx.send(Err(PublishError::LimitReached)); return; @@ -119,12 +123,12 @@ impl Manager { self.event_in_tx.downgrade(), )); - let publish_requested = events::PublishRequestEvent { + let event = SfuPublishRequest { handle, name: event.options.name, uses_e2ee: self.encryption_provider.is_some(), }; - _ = self.event_out_tx.send(publish_requested.into()).await; + _ = self.event_out_tx.send(event.into()).await; } /// Task that awaits a pending publish result. @@ -145,19 +149,19 @@ impl Manager { } _ = forward_result_tx.closed() => { let Some(tx) = event_in_tx.upgrade() else { return }; - let event = events::PublishCancelledEvent { handle }; + let event = PublishCancelled { handle }; _ = tx.try_send(event.into()); } } } - async fn handle_publish_cancelled(&mut self, event: events::PublishCancelledEvent) { + async fn handle_publish_cancelled(&mut self, event: PublishCancelled) { if self.descriptors.remove(&event.handle).is_none() { log::warn!("No descriptor for {}", event.handle); } } - async fn handle_publish_result(&mut self, event: events::PublishResultEvent) { + async fn handle_sfu_publish_response(&mut self, event: SfuPublishResponse) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { log::warn!("No descriptor for {}", event.handle); return; @@ -204,7 +208,7 @@ impl Manager { LocalDataTrack::new(info, inner) } - async fn handle_unpublished(&mut self, event: events::UnpublishEvent) { + async fn handle_sfu_unpublish_response(&mut self, event: SfuUnpublishResponse) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { return; }; @@ -216,7 +220,7 @@ impl Manager { } if event.client_initiated { // Inform the SFU if client initiated. - let event = events::UnpublishRequestEvent { handle: event.handle }; + let event = SfuUnpublishRequest { handle: event.handle }; _ = self.event_out_tx.send(event.into()).await; } } @@ -261,7 +265,7 @@ impl TrackTask { } } - let event = events::UnpublishEvent { handle: self.info.pub_handle, client_initiated: true }; + let event = SfuUnpublishResponse { handle: self.info.pub_handle, client_initiated: true }; _ = self.event_in_tx.send(event.into()).await; log::debug!("Track task ended: sid={}", self.info.sid); @@ -316,11 +320,9 @@ impl ManagerInput { options: DataTrackOptions, ) -> Result { let (result_tx, result_rx) = oneshot::channel(); - let event = events::PublishEvent { options, result_tx }; - self.event_in_tx - .try_send(event.into()) - .map_err(|_| PublishError::Disconnected)?; + let event = PublishRequest { options, result_tx }; + self.event_in_tx.try_send(event.into()).map_err(|_| PublishError::Disconnected)?; let track = tokio::time::timeout(Self::PUBLISH_TIMEOUT, result_rx) .await @@ -340,7 +342,7 @@ mod tests { use crate::{api::DataTrackSid, packet::Packet}; use fake::{Fake, Faker}; use futures_util::StreamExt; - use livekit_runtime::{timeout, sleep}; + use livekit_runtime::{sleep, timeout}; #[tokio::test] async fn test_task_shutdown() { @@ -371,7 +373,7 @@ mod tests { let mut packets_sent = 0; while let Some(event) = output.next().await { match event { - OutputEvent::PublishRequest(event) => { + OutputEvent::SfuPublishRequest(event) => { assert!(!event.uses_e2ee); assert_eq!(event.name, track_name_clone); @@ -382,9 +384,8 @@ mod tests { name: event.name, uses_e2ee: event.uses_e2ee, }; - let input_event = - events::PublishResultEvent { handle: event.handle, result: Ok(info) }; - _ = input.send(input_event.into()); + let event = SfuPublishResponse { handle: event.handle, result: Ok(info) }; + _ = input.send(event.into()); } OutputEvent::PacketsAvailable(packets) => { let packet = packets.into_iter().nth(0).unwrap(); @@ -392,7 +393,7 @@ mod tests { assert_eq!(payload.len(), payload_size); packets_sent += 1; } - OutputEvent::UnpublishRequest(event) => { + OutputEvent::SfuUnpublishRequest(event) => { assert_eq!(event.handle, pub_handle); assert_eq!(packets_sent, packet_count); break; diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index 6b65dc479..a86bd00b0 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::events; +use super::events::*; use crate::{ api::{DataTrackInfo, DataTrackSid, InternalError, PublishError}, packet::Handle, @@ -23,23 +23,23 @@ use std::mem; // MARK: - Output event -> protocol -impl From for proto::PublishDataTrackRequest { - fn from(event: events::PublishRequestEvent) -> Self { +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 } } } -impl From for proto::UnpublishDataTrackRequest { - fn from(event: events::UnpublishRequestEvent) -> Self { +impl From for proto::UnpublishDataTrackRequest { + fn from(event: SfuUnpublishRequest) -> Self { Self { pub_handle: event.handle.into() } } } // MARK: - Protocol -> input event -impl TryFrom for events::PublishResultEvent { +impl TryFrom for SfuPublishResponse { type Error = InternalError; fn try_from(msg: proto::PublishDataTrackResponse) -> Result { @@ -48,7 +48,7 @@ impl TryFrom for events::PublishResultEvent { } } -impl TryFrom for events::UnpublishEvent { +impl TryFrom for SfuUnpublishResponse { type Error = InternalError; fn try_from(msg: proto::UnpublishDataTrackResponse) -> Result { @@ -75,7 +75,7 @@ impl TryFrom for DataTrackInfo { pub fn publish_result_from_request_response( msg: &proto::RequestResponse, -) -> Option { +) -> Option { use proto::request_response::{Reason, Request}; let Some(request) = &msg.request else { return None }; let Request::PublishDataTrack(request) = request else { return None }; @@ -87,16 +87,16 @@ pub fn publish_result_from_request_response( Reason::DuplicateName => PublishError::DuplicateName, _ => PublishError::Internal(anyhow!("SFU rejected: {}", msg.message).into()), }; - let event = events::PublishResultEvent { handle, result: Err(error) }; + let event = SfuPublishResponse { handle, result: Err(error) }; Some(event) } pub fn publish_results_from_sync_state( msg: &mut proto::SyncState, -) -> Result, InternalError> { +) -> Result, InternalError> { mem::take(&mut msg.publish_data_tracks) .into_iter() - .map(TryInto::::try_into) + .map(TryInto::::try_into) .collect::, InternalError>>() } @@ -106,7 +106,7 @@ mod tests { #[test] fn test_from_publish_request_event() { - let event = events::PublishRequestEvent { + let event = SfuPublishRequest { handle: 1u32.try_into().unwrap(), name: "track".into(), uses_e2ee: true, @@ -119,7 +119,7 @@ mod tests { #[test] fn test_from_unpublish_request_event() { - let event = events::UnpublishRequestEvent { handle: 1u32.try_into().unwrap() }; + let event = SfuUnpublishRequest { handle: 1u32.try_into().unwrap() }; let request: proto::UnpublishDataTrackRequest = event.into(); assert_eq!(request.pub_handle, 1); } @@ -135,7 +135,7 @@ mod tests { } .into(), }; - let event: events::PublishResultEvent = response.try_into().unwrap(); + let event: SfuPublishResponse = response.try_into().unwrap(); assert_eq!(event.handle, 1u32.try_into().unwrap()); let info = event.result.expect("Expected ok result"); diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 5a15d55d4..30395d424 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -839,12 +839,12 @@ impl SessionInner { async fn forward_local_dt_event(self: Arc, event: dt::local::OutputEvent) { use dt::local::OutputEvent; match event { - OutputEvent::PublishRequest(event) => { + OutputEvent::SfuPublishRequest(event) => { self.signal_client .send(proto::signal_request::Message::PublishDataTrackRequest(event.into())) .await } - OutputEvent::UnpublishRequest(event) => { + OutputEvent::SfuUnpublishRequest(event) => { self.signal_client .send(proto::signal_request::Message::UnpublishDataTrackRequest(event.into())) .await @@ -1151,11 +1151,11 @@ impl SessionInner { } } proto::signal_response::Message::PublishDataTrackResponse(publish_res) => { - let event: dt::local::PublishResultEvent = publish_res.try_into()?; + let event: dt::local::SfuPublishResponse = publish_res.try_into()?; _ = self.local_dt_input.send(event.into()); } proto::signal_response::Message::UnpublishDataTrackResponse(unpublish_res) => { - let event: dt::local::UnpublishEvent = unpublish_res.try_into()?; + let event: dt::local::SfuUnpublishResponse = unpublish_res.try_into()?; _ = self.local_dt_input.send(event.into()); } proto::signal_response::Message::DataTrackSubscriberHandles(subscriber_handles) => { diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index ecf7fd0ed..80fe97d3b 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -44,7 +44,7 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { let mut rooms = test_rooms(2).await?; let (pub_room, _) = rooms.pop().unwrap(); - let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); + let (sub_room, mut sub_room_event_rx) = rooms.pop().unwrap(); let pub_identity = pub_room.local_participant().identity(); let frame_count = (PUBLISH_DURATION.as_secs_f64() * publish_fps).round() as u64; @@ -86,6 +86,8 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { let mut subscription = track.subscribe().await?; + //sub_room.simulate_scenario(livekit::SimulateScenario::SignalReconnect).await.unwrap(); + let mut recv_count = 0; while let Some(frame) = subscription.next().await { @@ -94,6 +96,9 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { assert!(payload.iter().all(|byte| byte == first_byte)); } assert_eq!(frame.user_timestamp(), None); + // if recv_count == 100 { + // sub_room.simulate_scenario(livekit::SimulateScenario::SignalReconnect).await.unwrap(); + // } recv_count += 1; } From cc5042e5bc46a467873588a10bdd6941a3d10d35 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:18:52 +1100 Subject: [PATCH 214/232] Rename handlers (local) --- livekit-datatrack/src/local/manager.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index e549846c5..d08a24732 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -85,13 +85,11 @@ impl Manager { while let Some(event) = self.event_in_rx.recv().await { log::debug!("Input event: {:?}", event); match event { - InputEvent::PublishRequest(event) => self.handle_publish_request(event).await, - InputEvent::PublishCancelled(event) => self.handle_publish_cancelled(event).await, - InputEvent::SfuPublishResponse(event) => { - self.handle_sfu_publish_response(event).await - } + InputEvent::PublishRequest(event) => self.on_publish_request(event).await, + InputEvent::PublishCancelled(event) => self.on_publish_cancelled(event).await, + InputEvent::SfuPublishResponse(event) => self.on_sfu_publish_response(event).await, InputEvent::SfuUnpublishResponse(event) => { - self.handle_sfu_unpublish_response(event).await + self.on_sfu_unpublish_response(event).await } InputEvent::Shutdown => break, } @@ -100,7 +98,7 @@ impl Manager { log::debug!("Task ended"); } - async fn handle_publish_request(&mut self, event: PublishRequest) { + async fn on_publish_request(&mut self, event: PublishRequest) { let Some(handle) = self.handle_allocator.get() else { _ = event.result_tx.send(Err(PublishError::LimitReached)); return; @@ -155,13 +153,13 @@ impl Manager { } } - async fn handle_publish_cancelled(&mut self, event: PublishCancelled) { + async fn on_publish_cancelled(&mut self, event: PublishCancelled) { if self.descriptors.remove(&event.handle).is_none() { log::warn!("No descriptor for {}", event.handle); } } - async fn handle_sfu_publish_response(&mut self, event: SfuPublishResponse) { + async fn on_sfu_publish_response(&mut self, event: SfuPublishResponse) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { log::warn!("No descriptor for {}", event.handle); return; @@ -208,7 +206,7 @@ impl Manager { LocalDataTrack::new(info, inner) } - async fn handle_sfu_unpublish_response(&mut self, event: SfuUnpublishResponse) { + async fn on_sfu_unpublish_response(&mut self, event: SfuUnpublishResponse) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { return; }; From d5433094873869f54d0dd54f8299f19a6f65d0a2 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:27:11 +1100 Subject: [PATCH 215/232] Organize --- livekit-datatrack/src/local/events.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/livekit-datatrack/src/local/events.rs b/livekit-datatrack/src/local/events.rs index d44ce685e..a2bc368fe 100644 --- a/livekit-datatrack/src/local/events.rs +++ b/livekit-datatrack/src/local/events.rs @@ -20,8 +20,6 @@ use bytes::Bytes; use from_variants::FromVariants; use tokio::sync::oneshot; -// MARK: - Input events - /// An external event handled by [`super::manager::Manager`]. #[derive(Debug, FromVariants)] pub enum InputEvent { @@ -33,6 +31,17 @@ pub enum InputEvent { Shutdown, } +/// An event produced by [`super::manager::Manager`] requiring external action. +#[derive(Debug, FromVariants)] +pub enum OutputEvent { + SfuPublishRequest(SfuPublishRequest), + SfuUnpublishRequest(SfuUnpublishRequest), + /// Serialized packets are ready to be sent over the transport. + PacketsAvailable(Vec), +} + +// MARK: - Input events + /// User requested to publish a track. #[derive(Debug)] pub struct PublishRequest { @@ -70,15 +79,6 @@ pub struct SfuUnpublishResponse { // MARK: - Output events -/// An event produced by [`super::manager::Manager`] requiring external action. -#[derive(Debug, FromVariants)] -pub enum OutputEvent { - SfuPublishRequest(SfuPublishRequest), - SfuUnpublishRequest(SfuUnpublishRequest), - /// Serialized packets are ready to be sent over the transport. - PacketsAvailable(Vec), -} - /// Request sent to the SFU to publish a track. #[derive(Debug)] pub struct SfuPublishRequest { From 0582e23ed7c6676965a49ef010fecd874a70e4e0 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:29:34 +1100 Subject: [PATCH 216/232] Events in separate module (remote) --- livekit-datatrack/src/lib.rs | 8 +-- livekit-datatrack/src/remote/events.rs | 94 +++++++++++++++++++++++++ livekit-datatrack/src/remote/manager.rs | 70 +----------------- livekit-datatrack/src/remote/mod.rs | 7 +- livekit-datatrack/src/remote/proto.rs | 2 +- 5 files changed, 102 insertions(+), 79 deletions(-) create mode 100644 livekit-datatrack/src/remote/events.rs diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index 9785ba5b2..86baf3004 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -46,13 +46,9 @@ pub mod internal { pub use crate::e2ee::*; pub mod local { - pub use crate::local::{ - events::*, - manager::*, - proto::*, - }; + pub use crate::local::{events::*, manager::*, proto::*}; } pub mod remote { - pub use crate::remote::{manager::*, proto::*}; + pub use crate::remote::{events::*, manager::*, proto::*}; } } diff --git a/livekit-datatrack/src/remote/events.rs b/livekit-datatrack/src/remote/events.rs new file mode 100644 index 000000000..f52f96cd1 --- /dev/null +++ b/livekit-datatrack/src/remote/events.rs @@ -0,0 +1,94 @@ +// Copyright 2025 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 crate::{ + api::{DataTrackFrame, DataTrackInfo, DataTrackSid, RemoteDataTrack, SubscribeError}, + packet::Handle, +}; +use bytes::Bytes; +use from_variants::FromVariants; +use std::collections::HashMap; +use tokio::sync::{broadcast, oneshot}; + +/// An external event handled by [`super::manager::Manager`]. +#[derive(Debug, FromVariants)] +pub enum InputEvent { + PublicationUpdates(PublicationUpdatesEvent), + Subscribe(SubscribeEvent), + SubscriberHandles(SubscriberHandlesEvent), + /// Packet has been received over the transport. + PacketReceived(Bytes), + Unsubscribe(UnsubscribeEvent), + /// Shutdown the manager, ending any subscriptions. + Shutdown, +} + +/// An event produced by [`super::manager::Manager`] requiring external action. +#[derive(Debug, FromVariants)] +pub enum OutputEvent { + SubscriptionUpdated(SubscriptionUpdatedEvent), + /// Remote track has been published and a track object has been created for + /// the user to interact with. + TrackAvailable(RemoteDataTrack), +} + +// MARK: - Input events + +/// Track publications by remote participants updated. +/// +/// This is used to detect newly published tracks as well as +/// tracks that have been unpublished. +/// +#[derive(Debug)] +pub struct PublicationUpdatesEvent { + /// Mapping between participant identity and data tracks published by that participant. + pub updates: HashMap>, +} + +pub(super) type SubscribeResult = Result, SubscribeError>; + +/// User requested to subscribe to a track. +#[derive(Debug)] +pub struct SubscribeEvent { + /// Identifier of the track. + pub(super) sid: DataTrackSid, + /// Async completion channel. + pub(super) result_tx: oneshot::Sender, +} + +/// Subscriber handles available or updated. +#[derive(Debug)] +pub struct SubscriberHandlesEvent { + /// Mapping between track handles attached to incoming packets to the + /// track SIDs they belong to. + pub mapping: HashMap, +} + +/// Unsubscribe from a track. +#[derive(Debug)] +pub struct UnsubscribeEvent { + /// Identifier of the track to unsubscribe from. + pub(super) sid: DataTrackSid, +} + +// MARK: - Output events + +/// User subscribed or unsubscribed to a track. +#[derive(Debug)] +pub struct SubscriptionUpdatedEvent { + /// Identifier of the affected track. + pub sid: DataTrackSid, + /// Whether to subscribe or unsubscribe. + pub subscribe: bool, +} diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index f9998e181..d06fa5b3f 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -13,6 +13,7 @@ // limitations under the License. use super::{ + events::*, pipeline::{Pipeline, PipelineOptions}, RemoteDataTrack, RemoteTrackInner, }; @@ -23,7 +24,6 @@ use crate::{ }; use anyhow::{anyhow, Context}; use bytes::Bytes; -use from_variants::FromVariants; use std::{ collections::{HashMap, HashSet}, mem, @@ -32,74 +32,6 @@ use std::{ use tokio::sync::{broadcast, mpsc, oneshot, watch}; use tokio_stream::{wrappers::ReceiverStream, Stream}; -/// An external event handled by [`Manager`]. -#[derive(Debug, FromVariants)] -pub enum InputEvent { - PublicationUpdates(PublicationUpdatesEvent), - Subscribe(SubscribeEvent), - SubscriberHandles(SubscriberHandlesEvent), - /// Packet has been received over the transport. - PacketReceived(Bytes), - Unsubscribe(UnsubscribeEvent), - /// Shutdown the manager, ending any subscriptions. - Shutdown, -} - -/// An event produced by [`Manager`] requiring external action. -#[derive(Debug, FromVariants)] -pub enum OutputEvent { - SubscriptionUpdated(SubscriptionUpdatedEvent), - /// Remote track has been published and a track object has been created for - /// the user to interact with. - TrackAvailable(RemoteDataTrack), -} - -/// Track publications by remote participants updated. -/// -/// This is used to detect newly published tracks as well as -/// tracks that have been unpublished. -/// -#[derive(Debug)] -pub struct PublicationUpdatesEvent { - /// Mapping between participant identity and data tracks published by that participant. - pub updates: HashMap>, -} - -/// Subscriber handles available or updated. -#[derive(Debug)] -pub struct SubscriberHandlesEvent { - /// Mapping between track handles attached to incoming packets to the - /// track SIDs they belong to. - pub mapping: HashMap, -} - -type SubscribeResult = Result, SubscribeError>; - -/// User requested to subscribe to a track. -#[derive(Debug)] -pub struct SubscribeEvent { - /// Identifier of the track. - pub(super) sid: DataTrackSid, - /// Async completion channel. - pub(super) result_tx: oneshot::Sender, -} - -/// User subscribed or unsubscribed to a track. -#[derive(Debug)] -pub struct SubscriptionUpdatedEvent { - /// Identifier of the affected track. - pub sid: DataTrackSid, - /// Whether to subscribe or unsubscribe. - pub subscribe: bool, -} - -/// Unsubscribe from a track. -#[derive(Debug)] -pub struct UnsubscribeEvent { - /// Identifier of the track to unsubscribe from. - sid: DataTrackSid, -} - /// Options for creating a [`Manager`]. #[derive(Debug)] pub struct ManagerOptions { diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index 9ef159d0c..b900df8db 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -16,14 +16,15 @@ use crate::api::{DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, Inter use anyhow::anyhow; use futures_util::StreamExt; use livekit_runtime::timeout; -use manager::SubscribeEvent; +use events::{InputEvent, SubscribeEvent}; use std::{marker::PhantomData, sync::Arc, time::Duration}; use thiserror::Error; use tokio::sync::{mpsc, oneshot, watch}; use tokio_stream::{wrappers::BroadcastStream, Stream}; -pub(crate) mod manager; pub(crate) mod proto; +pub(crate) mod events; +pub(crate) mod manager; mod depacketizer; mod pipeline; @@ -105,7 +106,7 @@ impl DataTrack { pub(crate) struct RemoteTrackInner { publisher_identity: Arc, published_rx: watch::Receiver, - event_in_tx: mpsc::WeakSender, + event_in_tx: mpsc::WeakSender, } impl RemoteTrackInner { diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index 530a7a808..ba10981df 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::manager::{PublicationUpdatesEvent, SubscriberHandlesEvent, SubscriptionUpdatedEvent}; +use super::events::*; use crate::{ api::{DataTrackInfo, DataTrackSid, InternalError}, packet::Handle, From ba3dec42f8a32c3cd3f414ceb87e27a128d7fa97 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:47:46 +1100 Subject: [PATCH 217/232] Rename events for clarity (remote) Rename --- livekit-datatrack/src/local/events.rs | 4 +- livekit-datatrack/src/remote/events.rs | 56 ++++++------ livekit-datatrack/src/remote/manager.rs | 108 ++++++++++++------------ livekit-datatrack/src/remote/mod.rs | 4 +- livekit-datatrack/src/remote/proto.rs | 18 ++-- livekit/src/rtc_engine/rtc_session.rs | 4 +- 6 files changed, 96 insertions(+), 98 deletions(-) diff --git a/livekit-datatrack/src/local/events.rs b/livekit-datatrack/src/local/events.rs index a2bc368fe..76176b78c 100644 --- a/livekit-datatrack/src/local/events.rs +++ b/livekit-datatrack/src/local/events.rs @@ -42,7 +42,7 @@ pub enum OutputEvent { // MARK: - Input events -/// User requested to publish a track. +/// Client requested to publish a track. #[derive(Debug)] pub struct PublishRequest { /// Publish options. @@ -51,7 +51,7 @@ pub struct PublishRequest { pub(super) result_tx: oneshot::Sender>, } -/// User request to publish a track has been cancelled. +/// Client request to publish a track has been cancelled. #[derive(Debug)] pub struct PublishCancelled { /// Publisher handle of the pending publication. diff --git a/livekit-datatrack/src/remote/events.rs b/livekit-datatrack/src/remote/events.rs index f52f96cd1..9e7f4175d 100644 --- a/livekit-datatrack/src/remote/events.rs +++ b/livekit-datatrack/src/remote/events.rs @@ -24,12 +24,12 @@ use tokio::sync::{broadcast, oneshot}; /// An external event handled by [`super::manager::Manager`]. #[derive(Debug, FromVariants)] pub enum InputEvent { - PublicationUpdates(PublicationUpdatesEvent), - Subscribe(SubscribeEvent), - SubscriberHandles(SubscriberHandlesEvent), + SubscribeRequest(SubscribeRequest), + UnsubscribeRequest(UnsubscribeRequest), + SfuPublicationUpdates(SfuPublicationUpdates), + SfuSubscriberHandles(SfuSubscriberHandles), /// Packet has been received over the transport. PacketReceived(Bytes), - Unsubscribe(UnsubscribeEvent), /// Shutdown the manager, ending any subscriptions. Shutdown, } @@ -37,7 +37,7 @@ pub enum InputEvent { /// An event produced by [`super::manager::Manager`] requiring external action. #[derive(Debug, FromVariants)] pub enum OutputEvent { - SubscriptionUpdated(SubscriptionUpdatedEvent), + SfuUpdateSubscription(SfuUpdateSubscription), /// Remote track has been published and a track object has been created for /// the user to interact with. TrackAvailable(RemoteDataTrack), @@ -45,48 +45,46 @@ pub enum OutputEvent { // MARK: - Input events -/// Track publications by remote participants updated. -/// -/// This is used to detect newly published tracks as well as -/// tracks that have been unpublished. -/// -#[derive(Debug)] -pub struct PublicationUpdatesEvent { - /// Mapping between participant identity and data tracks published by that participant. - pub updates: HashMap>, -} - +/// Result of a [`SubscribeRequest`]. pub(super) type SubscribeResult = Result, SubscribeError>; -/// User requested to subscribe to a track. +/// Client requested to subscribe to a data track. #[derive(Debug)] -pub struct SubscribeEvent { +pub struct SubscribeRequest { /// Identifier of the track. pub(super) sid: DataTrackSid, /// Async completion channel. pub(super) result_tx: oneshot::Sender, } -/// Subscriber handles available or updated. +/// Client requested to unsubscribe from a data track. #[derive(Debug)] -pub struct SubscriberHandlesEvent { - /// Mapping between track handles attached to incoming packets to the - /// track SIDs they belong to. - pub mapping: HashMap, +pub struct UnsubscribeRequest { + /// Identifier of the track to unsubscribe from. + pub(super) sid: DataTrackSid, } -/// Unsubscribe from a track. +/// SFU notification that remote participants have published or unpublished +/// unpublished data tracks. #[derive(Debug)] -pub struct UnsubscribeEvent { - /// Identifier of the track to unsubscribe from. - pub(super) sid: DataTrackSid, +pub struct SfuPublicationUpdates { + /// Mapping between participant identity and data tracks published by that participant. + pub updates: HashMap>, +} + +/// SFU notification that handles have been assigned for requested subscriptions. +#[derive(Debug)] +pub struct SfuSubscriberHandles { + /// Mapping between track handles attached to incoming packets to the + /// track SIDs they belong to. + pub mapping: HashMap, } // MARK: - Output events -/// User subscribed or unsubscribed to a track. +/// Request sent to the SFU to update the subscription for a data track. #[derive(Debug)] -pub struct SubscriptionUpdatedEvent { +pub struct SfuUpdateSubscription { /// Identifier of the affected track. pub sid: DataTrackSid, /// Whether to subscribe or unsubscribe. diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index d06fa5b3f..1ac0f9dca 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -96,13 +96,13 @@ impl Manager { log::debug!("Task started"); while let Some(event) = self.event_in_rx.recv().await { match event { - InputEvent::PublicationUpdates(event) => { + InputEvent::SubscribeRequest(event) => self.handle_subscribe(event).await, + InputEvent::UnsubscribeRequest(event) => self.handle_unsubscribe(event).await, + InputEvent::SfuPublicationUpdates(event) => { self.handle_publication_updates(event).await } - InputEvent::Subscribe(event) => self.handle_subscribe(event).await, - InputEvent::SubscriberHandles(event) => self.handle_subscriber_handles(event), + InputEvent::SfuSubscriberHandles(event) => self.handle_subscriber_handles(event), InputEvent::PacketReceived(bytes) => self.handle_packet_received(bytes), - InputEvent::Unsubscribe(event) => self.handle_unsubscribe(event).await, InputEvent::Shutdown => break, } } @@ -110,7 +110,50 @@ impl Manager { log::debug!("Task ended"); } - async fn handle_publication_updates(&mut self, event: PublicationUpdatesEvent) { + async fn handle_subscribe(&mut self, event: SubscribeRequest) { + let Some(descriptor) = self.descriptors.get_mut(&event.sid) else { + let error = + SubscribeError::Internal(anyhow!("Cannot subscribe to unknown track").into()); + _ = event.result_tx.send(Err(error)); + return; + }; + match &mut descriptor.subscription { + SubscriptionState::None => { + let update_event = + SfuUpdateSubscription { sid: event.sid.clone(), subscribe: true }; + _ = self.event_out_tx.send(update_event.into()).await; + descriptor.subscription = SubscriptionState::Pending { + result_txs: vec![event.result_tx], + }; + // TODO: schedule timeout internally + } + SubscriptionState::Pending { result_txs } => { + result_txs.push(event.result_tx); + } + SubscriptionState::Active { frame_tx, .. } => { + let frame_rx = frame_tx.subscribe(); + _ = event.result_tx.send(Ok(frame_rx)) + } + } + } + + async fn handle_unsubscribe(&mut self, event: UnsubscribeRequest) { + let Some(descriptor) = self.descriptors.get_mut(&event.sid) else { + return; + }; + + let SubscriptionState::Active { sub_handle, .. } = descriptor.subscription else { + log::warn!("Unexpected state"); + return; + }; + descriptor.subscription = SubscriptionState::None; + self.sub_handles.remove(&sub_handle); + + let event = SfuUpdateSubscription { sid: event.sid, subscribe: false }; + _ = self.event_out_tx.send(event.into()).await; + } + + async fn handle_publication_updates(&mut self, event: SfuPublicationUpdates) { if event.updates.is_empty() { return; } @@ -173,34 +216,7 @@ impl Manager { _ = descriptor.published_tx.send(false); } - async fn handle_subscribe(&mut self, event: SubscribeEvent) { - let Some(descriptor) = self.descriptors.get_mut(&event.sid) else { - let error = - SubscribeError::Internal(anyhow!("Cannot subscribe to unknown track").into()); - _ = event.result_tx.send(Err(error)); - return; - }; - match &mut descriptor.subscription { - SubscriptionState::None => { - let update_event = - SubscriptionUpdatedEvent { sid: event.sid.clone(), subscribe: true }; - _ = self.event_out_tx.send(update_event.into()).await; - descriptor.subscription = SubscriptionState::Pending { - result_txs: vec![event.result_tx], - }; - // TODO: schedule timeout internally - } - SubscriptionState::Pending { result_txs } => { - result_txs.push(event.result_tx); - } - SubscriptionState::Active { frame_tx, .. } => { - let frame_rx = frame_tx.subscribe(); - _ = event.result_tx.send(Ok(frame_rx)) - } - } - } - - fn handle_subscriber_handles(&mut self, event: SubscriberHandlesEvent) { + fn handle_subscriber_handles(&mut self, event: SfuSubscriberHandles) { for (handle, sid) in event.mapping { self.register_subscriber_handle(handle, sid); } @@ -283,22 +299,6 @@ impl Manager { .inspect_err(|err| log::debug!("Cannot send packet to track pipeline: {}", err)); } - async fn handle_unsubscribe(&mut self, event: UnsubscribeEvent) { - let Some(descriptor) = self.descriptors.get_mut(&event.sid) else { - return; - }; - - let SubscriptionState::Active { sub_handle, .. } = descriptor.subscription else { - log::warn!("Unexpected state"); - return; - }; - descriptor.subscription = SubscriptionState::None; - self.sub_handles.remove(&sub_handle); - - let event = SubscriptionUpdatedEvent { sid: event.sid, subscribe: false }; - _ = self.event_out_tx.send(event.into()).await; - } - /// Performs cleanup before the task ends. async fn shutdown(self) { for (_, descriptor) in self.descriptors { @@ -362,7 +362,7 @@ impl TrackTask { is_published = *self.published_rx.borrow(); }, _ = self.frame_tx.closed() => { - let event = UnsubscribeEvent { sid: self.info.sid.clone() }; + let event = UnsubscribeRequest { sid: self.info.sid.clone() }; _ = self.event_in_tx.send(event.into()).await; break; // No more subscribers }, @@ -452,7 +452,7 @@ mod tests { mem::drop(frame_rx); while let Some(event) = event_in_rx.recv().await { - let InputEvent::Unsubscribe(event) = event else { + let InputEvent::UnsubscribeRequest(event) = event else { panic!("Unexpected event type"); }; assert_eq!(event.sid, sid); @@ -475,7 +475,7 @@ mod tests { livekit_runtime::spawn(manager.run()); // Simulate track published - let event = PublicationUpdatesEvent { + let event = SfuPublicationUpdates { updates: HashMap::from([( publisher_identity.clone(), vec![DataTrackInfo { @@ -507,13 +507,13 @@ mod tests { let simulate_subscriber_handles = async { while let Some(event) = output.next().await { match event { - OutputEvent::SubscriptionUpdated(event) => { + OutputEvent::SfuUpdateSubscription(event) => { assert!(event.subscribe); assert_eq!(event.sid, track_sid); time::sleep(Duration::from_millis(20)).await; // Simulate SFU reply - let event = SubscriberHandlesEvent { + let event = SfuSubscriberHandles { mapping: HashMap::from([(sub_handle, track_sid.clone())]), }; _ = input.send(event.into()); diff --git a/livekit-datatrack/src/remote/mod.rs b/livekit-datatrack/src/remote/mod.rs index b900df8db..8b78f1cc5 100644 --- a/livekit-datatrack/src/remote/mod.rs +++ b/livekit-datatrack/src/remote/mod.rs @@ -16,7 +16,7 @@ use crate::api::{DataTrack, DataTrackFrame, DataTrackInfo, DataTrackInner, Inter use anyhow::anyhow; use futures_util::StreamExt; use livekit_runtime::timeout; -use events::{InputEvent, SubscribeEvent}; +use events::{InputEvent, SubscribeRequest}; use std::{marker::PhantomData, sync::Arc, time::Duration}; use thiserror::Error; use tokio::sync::{mpsc, oneshot, watch}; @@ -74,7 +74,7 @@ impl DataTrack { /// pub async fn subscribe(&self) -> Result, SubscribeError> { let (result_tx, result_rx) = oneshot::channel(); - let subscribe_event = SubscribeEvent { sid: self.info.sid.clone(), result_tx }; + let subscribe_event = SubscribeRequest { sid: self.info.sid.clone(), result_tx }; self.inner() .event_in_tx .upgrade() diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index ba10981df..19d91f610 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -22,7 +22,7 @@ use std::{collections::HashMap, mem}; // MARK: - Protocol -> input event -impl TryFrom for SubscriberHandlesEvent { +impl TryFrom for SfuSubscriberHandles { type Error = InternalError; fn try_from(msg: proto::DataTrackSubscriberHandles) -> Result { @@ -35,7 +35,7 @@ impl TryFrom for SubscriberHandlesEvent { Ok((handle, sid)) }) .collect::, _>>()?; - Ok(SubscriberHandlesEvent { mapping }) + Ok(SfuSubscriberHandles { mapping }) } } @@ -46,7 +46,7 @@ impl TryFrom for SubscriberHandlesEvent { /// pub fn event_from_join( msg: &mut proto::JoinResponse, -) -> Result { +) -> Result { event_from_participant_info(&mut msg.other_participants, None) } @@ -58,7 +58,7 @@ pub fn event_from_join( pub fn event_from_participant_update( msg: &mut proto::ParticipantUpdate, local_participant_identity: &str, -) -> Result { +) -> Result { // TODO: is there a better way to exclude the local participant? event_from_participant_info(&mut msg.participants, local_participant_identity.into()) } @@ -71,7 +71,7 @@ pub fn event_from_participant_update( fn event_from_participant_info( msg: &mut [ParticipantInfo], local_participant_identity: Option<&str>, -) -> Result { +) -> Result { let updates = msg .iter_mut() .filter(|participant| { @@ -81,7 +81,7 @@ fn event_from_participant_info( Ok((participant.identity.clone(), extract_track_info(participant)?)) }) .collect::>, _>>()?; - Ok(PublicationUpdatesEvent { updates }) + Ok(SfuPublicationUpdates { updates }) } fn extract_track_info(msg: &mut ParticipantInfo) -> Result, InternalError> { @@ -93,8 +93,8 @@ fn extract_track_info(msg: &mut ParticipantInfo) -> Result, I // MARK: - Output event -> protocol -impl From for proto::UpdateDataSubscription { - fn from(event: SubscriptionUpdatedEvent) -> Self { +impl From for proto::UpdateDataSubscription { + fn from(event: SfuUpdateSubscription) -> Self { let update = proto::update_data_subscription::Update { track_sid: event.sid.into(), subscribe: event.subscribe, @@ -129,7 +129,7 @@ mod tests { let subscriber_handles = proto::DataTrackSubscriberHandles { sub_handles: HashMap::from(sub_handles) }; - let event: SubscriberHandlesEvent = subscriber_handles.try_into().unwrap(); + let event: SfuSubscriberHandles = subscriber_handles.try_into().unwrap(); assert_eq!( event.mapping.get(&1u32.try_into().unwrap()).unwrap(), &"DTR_1234".to_string().try_into().unwrap() diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 30395d424..f796adcea 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -863,7 +863,7 @@ impl SessionInner { async fn forward_remote_dt_event(self: Arc, event: dt::remote::OutputEvent) { use dt::remote::OutputEvent; match event { - OutputEvent::SubscriptionUpdated(event) => { + OutputEvent::SfuUpdateSubscription(event) => { self.signal_client .send(proto::signal_request::Message::UpdateDataSubscription(event.into())) .await @@ -1159,7 +1159,7 @@ impl SessionInner { _ = self.local_dt_input.send(event.into()); } proto::signal_response::Message::DataTrackSubscriberHandles(subscriber_handles) => { - let event: dt::remote::SubscriberHandlesEvent = subscriber_handles.try_into()?; + let event: dt::remote::SfuSubscriberHandles = subscriber_handles.try_into()?; _ = self.remote_dt_input.send(event.into()); } proto::signal_response::Message::RefreshToken(ref token) => { From f0800248de8a138d329af235730b76e3ceb08c24 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:52:54 +1100 Subject: [PATCH 218/232] Rename handlers (remote) --- livekit-datatrack/src/remote/manager.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index 1ac0f9dca..f333b87aa 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -96,13 +96,13 @@ impl Manager { log::debug!("Task started"); while let Some(event) = self.event_in_rx.recv().await { match event { - InputEvent::SubscribeRequest(event) => self.handle_subscribe(event).await, - InputEvent::UnsubscribeRequest(event) => self.handle_unsubscribe(event).await, + InputEvent::SubscribeRequest(event) => self.on_subscribe_request(event).await, + InputEvent::UnsubscribeRequest(event) => self.on_unsubscribe_request(event).await, InputEvent::SfuPublicationUpdates(event) => { - self.handle_publication_updates(event).await + self.on_sfu_publication_updates(event).await } - InputEvent::SfuSubscriberHandles(event) => self.handle_subscriber_handles(event), - InputEvent::PacketReceived(bytes) => self.handle_packet_received(bytes), + InputEvent::SfuSubscriberHandles(event) => self.on_sfu_subscriber_handles(event), + InputEvent::PacketReceived(bytes) => self.on_packet_received(bytes), InputEvent::Shutdown => break, } } @@ -110,7 +110,7 @@ impl Manager { log::debug!("Task ended"); } - async fn handle_subscribe(&mut self, event: SubscribeRequest) { + async fn on_subscribe_request(&mut self, event: SubscribeRequest) { let Some(descriptor) = self.descriptors.get_mut(&event.sid) else { let error = SubscribeError::Internal(anyhow!("Cannot subscribe to unknown track").into()); @@ -122,9 +122,8 @@ impl Manager { let update_event = SfuUpdateSubscription { sid: event.sid.clone(), subscribe: true }; _ = self.event_out_tx.send(update_event.into()).await; - descriptor.subscription = SubscriptionState::Pending { - result_txs: vec![event.result_tx], - }; + descriptor.subscription = + SubscriptionState::Pending { result_txs: vec![event.result_tx] }; // TODO: schedule timeout internally } SubscriptionState::Pending { result_txs } => { @@ -137,7 +136,7 @@ impl Manager { } } - async fn handle_unsubscribe(&mut self, event: UnsubscribeRequest) { + async fn on_unsubscribe_request(&mut self, event: UnsubscribeRequest) { let Some(descriptor) = self.descriptors.get_mut(&event.sid) else { return; }; @@ -153,7 +152,7 @@ impl Manager { _ = self.event_out_tx.send(event.into()).await; } - async fn handle_publication_updates(&mut self, event: SfuPublicationUpdates) { + async fn on_sfu_publication_updates(&mut self, event: SfuPublicationUpdates) { if event.updates.is_empty() { return; } @@ -216,7 +215,7 @@ impl Manager { _ = descriptor.published_tx.send(false); } - fn handle_subscriber_handles(&mut self, event: SfuSubscriberHandles) { + fn on_sfu_subscriber_handles(&mut self, event: SfuSubscriberHandles) { for (handle, sid) in event.mapping { self.register_subscriber_handle(handle, sid); } @@ -274,7 +273,7 @@ impl Manager { } } - fn handle_packet_received(&mut self, bytes: Bytes) { + fn on_packet_received(&mut self, bytes: Bytes) { let packet = match Packet::deserialize(bytes) { Ok(packet) => packet, Err(err) => { From 28a650c325418b3bb8dfee66176ba6b1bbd26b20 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 24 Jan 2026 18:03:59 +1100 Subject: [PATCH 219/232] Make separate event --- livekit-datatrack/src/local/events.rs | 11 ++++++++--- livekit-datatrack/src/local/manager.rs | 21 ++++++++++++++------- livekit-datatrack/src/local/proto.rs | 2 +- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/livekit-datatrack/src/local/events.rs b/livekit-datatrack/src/local/events.rs index 76176b78c..cb4935b89 100644 --- a/livekit-datatrack/src/local/events.rs +++ b/livekit-datatrack/src/local/events.rs @@ -25,6 +25,7 @@ use tokio::sync::oneshot; pub enum InputEvent { PublishRequest(PublishRequest), PublishCancelled(PublishCancelled), + UnpublishRequest(UnpublishRequest), SfuPublishResponse(SfuPublishResponse), SfuUnpublishResponse(SfuUnpublishResponse), /// Shutdown the manager and all associated tracks. @@ -58,6 +59,13 @@ pub struct PublishCancelled { pub(super) handle: Handle, } +/// Client request to unpublish a track. +#[derive(Debug)] +pub struct UnpublishRequest { + /// Publisher handle of the track to unpublish. + pub(super) handle: Handle, +} + /// SFU responded to a request to publish a data track. #[derive(Debug)] pub struct SfuPublishResponse { @@ -72,9 +80,6 @@ pub struct SfuPublishResponse { pub struct SfuUnpublishResponse { /// Publisher handle of the track that was unpublished. pub handle: Handle, - /// Whether the unpublish was initiated by the client. - pub client_initiated: bool, - // TODO: this should be made into a separate event } // MARK: - Output events diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index d08a24732..37787b810 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -87,6 +87,7 @@ impl Manager { match event { InputEvent::PublishRequest(event) => self.on_publish_request(event).await, InputEvent::PublishCancelled(event) => self.on_publish_cancelled(event).await, + InputEvent::UnpublishRequest(event) => self.on_unpublish_request(event).await, InputEvent::SfuPublishResponse(event) => self.on_sfu_publish_response(event).await, InputEvent::SfuUnpublishResponse(event) => { self.on_sfu_unpublish_response(event).await @@ -159,6 +160,13 @@ impl Manager { } } + async fn on_unpublish_request(&mut self, event: UnpublishRequest) { + self.remove_descriptor(event.handle); + + let event = SfuUnpublishRequest { handle: event.handle }; + _ = self.event_out_tx.send(event.into()).await; + } + async fn on_sfu_publish_response(&mut self, event: SfuPublishResponse) { let Some(descriptor) = self.descriptors.remove(&event.handle) else { log::warn!("No descriptor for {}", event.handle); @@ -207,7 +215,11 @@ impl Manager { } async fn on_sfu_unpublish_response(&mut self, event: SfuUnpublishResponse) { - let Some(descriptor) = self.descriptors.remove(&event.handle) else { + self.remove_descriptor(event.handle); + } + + fn remove_descriptor(&mut self, handle: Handle) { + let Some(descriptor) = self.descriptors.remove(&handle) else { return; }; let Descriptor::Active { published_tx, .. } = descriptor else { @@ -216,11 +228,6 @@ impl Manager { if !*published_tx.borrow() { _ = published_tx.send(false); } - if event.client_initiated { - // Inform the SFU if client initiated. - let event = SfuUnpublishRequest { handle: event.handle }; - _ = self.event_out_tx.send(event.into()).await; - } } /// Performs cleanup before the task ends. @@ -263,7 +270,7 @@ impl TrackTask { } } - let event = SfuUnpublishResponse { handle: self.info.pub_handle, client_initiated: true }; + let event = UnpublishRequest { handle: self.info.pub_handle }; _ = self.event_in_tx.send(event.into()).await; log::debug!("Track task ended: sid={}", self.info.sid); diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index a86bd00b0..6ed7a6ab9 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -54,7 +54,7 @@ impl TryFrom for SfuUnpublishResponse { fn try_from(msg: proto::UnpublishDataTrackResponse) -> Result { let handle: Handle = msg.info.context("Missing info")?.pub_handle.try_into().map_err(anyhow::Error::from)?; - Ok(Self { handle, client_initiated: false }) + Ok(Self { handle }) } } From 7abbf0b3495dfc4eb2bae3f2e22b5653850c1fe4 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:35:57 +1100 Subject: [PATCH 220/232] Move data track managers to room --- livekit/src/room/mod.rs | 112 +++++++- .../src/room/participant/local_participant.rs | 6 +- livekit/src/rtc_engine/mod.rs | 32 ++- livekit/src/rtc_engine/rtc_session.rs | 243 ++++++------------ 4 files changed, 214 insertions(+), 179 deletions(-) diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index 59ee68d17..bafa35de9 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. use bmrng::unbounded::UnboundedRequestReceiver; +use futures_util::{Stream, StreamExt}; use libwebrtc::{ native::frame_cryptor::EncryptionState, prelude::{ @@ -23,7 +24,7 @@ use libwebrtc::{ RtcError, }; use livekit_api::signal_client::{SignalOptions, SignalSdkOptions}; -use livekit_datatrack::api::RemoteDataTrack; +use livekit_datatrack::{api::RemoteDataTrack, internal as dt}; use livekit_protocol::observer::Dispatcher; use livekit_protocol::{self as proto, encryption}; use livekit_runtime::JoinHandle; @@ -46,6 +47,7 @@ pub use self::{ }; pub use crate::rtc_engine::SimulateScenario; use crate::{ + e2ee::data_track::{DataTrackDecryptionProvider, DataTrackEncryptionProvider}, participant::ConnectionQuality, prelude::*, registered_audio_filter_plugins, @@ -443,6 +445,8 @@ pub(crate) struct RoomSession { e2ee_manager: E2eeManager, incoming_stream_manager: IncomingStreamManager, outgoing_stream_manager: OutgoingStreamManager, + local_dt_input: dt::local::ManagerInput, + remote_dt_input: dt::remote::ManagerInput, handle: AsyncMutex>, } @@ -450,6 +454,10 @@ struct Handle { room_handle: JoinHandle<()>, incoming_stream_handle: JoinHandle<()>, outgoing_stream_handle: JoinHandle<()>, + local_dt_task: JoinHandle<()>, + local_dt_forward_task: JoinHandle<()>, + remote_dt_task: JoinHandle<()>, + remote_dt_forward_task: JoinHandle<()>, close_tx: broadcast::Sender<()>, } @@ -471,13 +479,16 @@ impl Room { mut options: RoomOptions, ) -> RoomResult<(Self, mpsc::UnboundedReceiver)> { // TODO(theomonnom): move connection logic to the RoomSession + let with_dc_encryption = options.encryption.is_some(); let encryption_options = options.encryption.take().or(options.e2ee.take()); let e2ee_manager = E2eeManager::new(encryption_options, with_dc_encryption); + let mut signal_options = SignalOptions::default(); signal_options.sdk_options = options.sdk_options.clone().into(); signal_options.auto_subscribe = options.auto_subscribe; signal_options.adaptive_stream = options.adaptive_stream; + let (rtc_engine, join_response, engine_events) = RtcEngine::connect( url, token, @@ -580,6 +591,25 @@ impl Room { } }); + let decryption_provider = e2ee_manager.enabled().then(|| { + Arc::new(DataTrackEncryptionProvider::new( + e2ee_manager.clone(), + local_participant.identity().clone(), + )) as Arc + }); + let encryption_provider = e2ee_manager.enabled().then(|| { + Arc::new(DataTrackDecryptionProvider::new(e2ee_manager.clone())) + as Arc + }); + + let local_dt_options = dt::local::ManagerOptions { decryption_provider }; + let (local_dt_manager, local_dt_input, local_dt_output) = + dt::local::Manager::new(local_dt_options); + + let remote_dt_options = dt::remote::ManagerOptions { encryption_provider }; + let (remote_dt_manager, remote_dt_input, remote_dt_output) = + dt::remote::Manager::new(remote_dt_options); + let (incoming_stream_manager, open_rx) = IncomingStreamManager::new(); let (outgoing_stream_manager, packet_rx) = OutgoingStreamManager::new(); @@ -610,6 +640,8 @@ impl Room { e2ee_manager: e2ee_manager.clone(), incoming_stream_manager, outgoing_stream_manager, + local_dt_input, + remote_dt_input, handle: Default::default(), }); inner.local_participant.set_session(Arc::downgrade(&inner)); @@ -679,10 +711,28 @@ impl Room { close_rx.resubscribe(), )); + let local_dt_task = livekit_runtime::spawn(local_dt_manager.run()); + let local_dt_forward_task = livekit_runtime::spawn( + inner.clone().local_dt_forward_task(local_dt_output, close_rx.resubscribe()), + ); + + let remote_dt_task = livekit_runtime::spawn(remote_dt_manager.run()); + let remote_dt_forward_task = livekit_runtime::spawn( + inner.clone().remote_dt_forward_task(remote_dt_output, close_rx.resubscribe()), + ); + let room_handle = livekit_runtime::spawn(inner.clone().room_task(engine_events, close_rx)); - let handle = - Handle { room_handle, incoming_stream_handle, outgoing_stream_handle, close_tx }; + let handle = Handle { + room_handle, + incoming_stream_handle, + outgoing_stream_handle, + local_dt_task, + local_dt_forward_task, + remote_dt_task, + remote_dt_forward_task, + close_tx, + }; inner.handle.lock().await.replace(handle); Ok((Self { inner }, events)) @@ -914,8 +964,11 @@ impl RoomSession { EngineEvent::RefreshToken { url, token } => { self.handle_refresh_token(url, token); } - EngineEvent::RemoteDataTrackPublished(track) => { - self.dispatcher.dispatch(&RoomEvent::RemoteDataTrackPublished(track)); + EngineEvent::LocalDataTrackInput(event) => { + _ = self.local_dt_input.send(event); + } + EngineEvent::RemoteDataTrackInput(event) => { + _ = self.remote_dt_input.send(event); } } @@ -936,6 +989,10 @@ impl RoomSession { let _ = handle.close_tx.send(()); let _ = handle.incoming_stream_handle.await; let _ = handle.outgoing_stream_handle.await; + let _ = handle.local_dt_forward_task.await; + let _ = handle.local_dt_task.await; + let _ = handle.remote_dt_forward_task.await; + let _ = handle.remote_dt_task.await; let _ = handle.room_handle.await; self.dispatcher.clear(); @@ -1752,6 +1809,51 @@ impl RoomSession { filter.update_token(url.clone(), token.clone()); } } + + /// Task for handling output events from the local data track manager. + async fn local_dt_forward_task( + self: Arc, + mut events: impl Stream + Unpin, + mut close_rx: broadcast::Receiver<()>, + ) { + loop { + tokio::select! { + event = events.next() => match event { + Some(event) => _ = self.rtc_engine.handle_local_data_track_output(event).await, + None => break, + }, + _ = close_rx.recv() => { + _ = self.local_dt_input.send(dt::local::InputEvent::Shutdown); + break; + }, + } + } + } + + /// Task for handling output events from the remote data track manager. + async fn remote_dt_forward_task( + self: Arc, + mut events: impl Stream + Unpin, + mut close_rx: broadcast::Receiver<()>, + ) { + loop { + tokio::select! { + event = events.next() => match event { + Some(event) => match event { + dt::remote::OutputEvent::TrackAvailable(track) => { + _ = self.dispatcher.dispatch(&RoomEvent::RemoteDataTrackPublished(track)); + } + other => _ = self.rtc_engine.handle_remote_data_track_output(other).await + }, + None => break, + }, + _ = close_rx.recv() => { + _ = self.remote_dt_input.send(dt::remote::InputEvent::Shutdown); + break; + }, + } + } + } } /// Receives stream readers for newly-opened streams and dispatches room events. diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 5d51abf86..4d5a7e2db 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -254,7 +254,11 @@ impl LocalParticipant { &self, options: impl Into, ) -> Result, data_track::PublishError> { - self.inner.rtc_engine.publish_data_track(options.into()).await + self.session() + .ok_or(PublishError::Disconnected)? + .local_dt_input + .publish_track(options.into()) + .await } /// Publishes a media track. diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index d2ced6d6f..20694dfda 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -14,7 +14,7 @@ use libwebrtc::prelude::*; use livekit_api::signal_client::{SignalError, SignalOptions}; -use livekit_datatrack::api::{DataTrackOptions, LocalDataTrack, PublishError, RemoteDataTrack}; +use livekit_datatrack::internal as dt; use livekit_protocol as proto; use livekit_runtime::{interval, Interval, JoinHandle}; use parking_lot::{RwLock, RwLockReadGuard}; @@ -185,7 +185,8 @@ pub enum EngineEvent { url: String, token: String, }, - RemoteDataTrackPublished(RemoteDataTrack), + LocalDataTrackInput(dt::local::InputEvent), + RemoteDataTrackInput(dt::remote::InputEvent), } /// Represents a running RtcSession with the ability to close the session @@ -268,16 +269,22 @@ impl RtcEngine { session.simulate_scenario(scenario).await } - pub async fn publish_data_track( - &self, - options: DataTrackOptions, - ) -> Result { + pub async fn handle_local_data_track_output(&self, event: dt::local::OutputEvent) -> EngineResult<()> { let (session, _r_lock) = { - let (handle, _r_lock) = - self.inner.wait_reconnection().await.map_err(|_| PublishError::Timeout)?; + let (handle, _r_lock) = self.inner.wait_reconnection().await?; (handle.session.clone(), _r_lock) }; - session.publish_data_track(options).await + session.handle_local_data_track_output(event).await; + Ok(()) + } + + pub async fn handle_remote_data_track_output(&self, event: dt::remote::OutputEvent) -> EngineResult<()> { + let (session, _r_lock) = { + let (handle, _r_lock) = self.inner.wait_reconnection().await?; + (handle.session.clone(), _r_lock) + }; + session.handle_remote_data_track_output(event).await; + Ok(()) } pub async fn add_track(&self, req: proto::AddTrackRequest) -> EngineResult { @@ -615,8 +622,11 @@ impl EngineInner { SessionEvent::RefreshToken { url, token } => { let _ = self.engine_tx.send(EngineEvent::RefreshToken { url, token }); } - SessionEvent::RemoteDataTrackPublished(track) => { - let _ = self.engine_tx.send(EngineEvent::RemoteDataTrackPublished(track)); + SessionEvent::LocalDataTrackInput(event) => { + let _ = self.engine_tx.send(EngineEvent::LocalDataTrackInput(event)); + } + SessionEvent::RemoteDataTrackInput(event) => { + let _ = self.engine_tx.send(EngineEvent::RemoteDataTrackInput(event)); } } Ok(()) diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index f796adcea..9f4e4169a 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -24,13 +24,9 @@ use std::{ }; use bytes::Bytes; -use futures_util::{Stream, StreamExt}; use libwebrtc::{prelude::*, stats::RtcStats}; use livekit_api::signal_client::{SignalClient, SignalEvent, SignalEvents}; -use livekit_datatrack::{ - api::{DataTrackOptions, LocalDataTrack, PublishError, RemoteDataTrack}, - internal as dt, -}; +use livekit_datatrack::internal as dt; use livekit_protocol::{self as proto}; use livekit_runtime::{sleep, JoinHandle}; use parking_lot::Mutex; @@ -40,11 +36,21 @@ use proto::{ SignalTarget, }; use serde::{Deserialize, Serialize}; -use tokio::sync::{mpsc, oneshot, watch, Notify}; +use tokio::sync::{ + mpsc::{self, WeakUnboundedSender}, + oneshot, watch, Notify, +}; use super::{rtc_events, EngineError, EngineOptions, EngineResult, SimulateScenario}; use crate::{ - e2ee::data_track::*, + id::ParticipantIdentity, + utils::{ + ttl_map::TtlMap, + tx_queue::{TxQueue, TxQueueItem}, + }, + ChatMessage, TranscriptionSegment, +}; +use crate::{ id::ParticipantSid, options::TrackPublishOptions, prelude::TrackKind, @@ -57,14 +63,6 @@ use crate::{ track::LocalTrack, DataPacketKind, }; -use crate::{ - id::ParticipantIdentity, - utils::{ - ttl_map::TtlMap, - tx_queue::{TxQueue, TxQueueItem}, - }, - ChatMessage, TranscriptionSegment, -}; pub const ICE_CONNECT_TIMEOUT: Duration = Duration::from_secs(15); pub const TRACK_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); @@ -202,7 +200,8 @@ pub enum SessionEvent { url: String, token: String, }, - RemoteDataTrackPublished(RemoteDataTrack), + LocalDataTrackInput(dt::local::InputEvent), + RemoteDataTrackInput(dt::remote::InputEvent), } #[derive(Debug)] @@ -378,10 +377,6 @@ struct SessionInner { pending_requests: Mutex>>, e2ee_manager: Option, - - // Data track managers - local_dt_input: dt::local::ManagerInput, - remote_dt_input: dt::remote::ManagerInput, } /// Information about the local participant needed for outgoing @@ -422,10 +417,6 @@ struct SessionHandle { signal_task: JoinHandle<()>, rtc_task: JoinHandle<()>, dc_task: JoinHandle<()>, - local_dt_task: JoinHandle<()>, - local_dt_forward_task: JoinHandle<()>, - remote_dt_task: JoinHandle<()>, - remote_dt_forward_task: JoinHandle<()>, } impl RtcSession { @@ -445,6 +436,9 @@ impl RtcSession { let Some(participant_info) = SessionParticipantInfo::from_join(&join_response) else { Err(EngineError::Internal("Join response missing participant info".into()))? }; + if let Ok(initial_publications) = dt::remote::event_from_join(&mut join_response) { + _ = emitter.send(SessionEvent::RemoteDataTrackInput(initial_publications.into())); + } let (rtc_emitter, rtc_events) = mpsc::unbounded_channel(); let rtc_config = make_rtc_config_join(join_response.clone(), options.rtc_config.clone()); @@ -474,7 +468,7 @@ impl RtcSession { .peer_connection() .create_data_channel(LOSSY_DC_LABEL, lossy_options.clone())?; - let mut dt_transport = publisher_pc + let dt_transport = publisher_pc .peer_connection() .create_data_channel(DATA_TRACK_DC_LABEL, lossy_options)?; @@ -484,26 +478,6 @@ impl RtcSession { rtc_events::forward_dc_events(&mut lossy_dc, DataPacketKind::Lossy, rtc_emitter.clone()); rtc_events::forward_dc_events(&mut reliable_dc, DataPacketKind::Reliable, rtc_emitter); - let local_dt_options = dt::local::ManagerOptions { - decryption_provider: e2ee_manager.clone().filter(|m| m.enabled()).map(|m| { - Arc::new(DataTrackEncryptionProvider::new(m, participant_info.identity.clone())) - as Arc - }), - }; - let (local_dt_manager, local_dt_input, local_dt_output) = - dt::local::Manager::new(local_dt_options); - - let remote_dt_options = dt::remote::ManagerOptions { - encryption_provider: e2ee_manager.clone().filter(|m| m.enabled()).map(|m| { - Arc::new(DataTrackDecryptionProvider::new(m)) as Arc - }), - }; - let (remote_dt_manager, remote_dt_input, remote_dt_output) = - dt::remote::Manager::new(remote_dt_options); - if let Ok(initial_publications) = dt::remote::event_from_join(&mut join_response) { - _ = remote_dt_input.send(initial_publications.into()); - } - let (close_tx, close_rx) = watch::channel(false); let inner = Arc::new(SessionInner { @@ -536,8 +510,6 @@ impl RtcSession { negotiation_queue: NegotiationQueue::new(), pending_requests: Default::default(), e2ee_manager, - local_dt_input, - remote_dt_input, }); // Start session tasks @@ -548,26 +520,7 @@ impl RtcSession { let dc_task = livekit_runtime::spawn(inner.clone().data_channel_task(dc_events, close_rx.clone())); - let local_dt_forward_task = - livekit_runtime::spawn(inner.clone().local_dt_forward_task(local_dt_output)); - let local_dt_task = livekit_runtime::spawn(local_dt_manager.run()); - - let remote_dt_forward_task = - livekit_runtime::spawn(inner.clone().remote_dt_forward_task(remote_dt_output)); - let remote_dt_task = livekit_runtime::spawn(remote_dt_manager.run()); - - // TODO: closure. - - let handle = Mutex::new(Some(SessionHandle { - close_tx, - signal_task, - rtc_task, - dc_task, - local_dt_task, - local_dt_forward_task, - remote_dt_task, - remote_dt_forward_task, - })); + let handle = Mutex::new(Some(SessionHandle { close_tx, signal_task, rtc_task, dc_task })); Ok((Self { inner, handle }, join_response, session_events)) } @@ -610,14 +563,6 @@ impl RtcSession { let _ = handle.rtc_task.await; let _ = handle.signal_task.await; let _ = handle.dc_task.await; - - _ = self.inner.local_dt_input.send(dt::local::InputEvent::Shutdown); - let _ = handle.local_dt_task.await; - let _ = handle.local_dt_forward_task.await; - - _ = self.inner.remote_dt_input.send(dt::remote::InputEvent::Shutdown); - let _ = handle.remote_dt_task.await; - let _ = handle.remote_dt_forward_task.await; } // Close the PeerConnections after the task @@ -625,13 +570,6 @@ impl RtcSession { self.inner.close().await; } - pub async fn publish_data_track( - &self, - options: DataTrackOptions, - ) -> Result { - self.inner.publish_data_track(options).await - } - pub async fn publish_data( &self, data: proto::DataPacket, @@ -681,6 +619,52 @@ impl RtcSession { self.inner.data_channel(target, kind) } + /// Handles an event from the local data track manager. + pub(super) async fn handle_local_data_track_output(&self, event: dt::local::OutputEvent) { + use dt::local::OutputEvent; + match event.into() { + OutputEvent::SfuPublishRequest(event) => { + self.signal_client() + .send(proto::signal_request::Message::PublishDataTrackRequest(event.into())) + .await + } + OutputEvent::SfuUnpublishRequest(event) => { + self.signal_client() + .send(proto::signal_request::Message::UnpublishDataTrackRequest(event.into())) + .await + } + OutputEvent::PacketsAvailable(packets) => self.try_send_data_track_packets(packets), + } + } + + /// Handles an event from the remote data track manager. + pub(super) async fn handle_remote_data_track_output(&self, event: dt::remote::OutputEvent) { + use dt::remote::OutputEvent; + match event.into() { + OutputEvent::SfuUpdateSubscription(event) => { + self.signal_client() + .send(proto::signal_request::Message::UpdateDataSubscription(event.into())) + .await + } + _ => {} + } + } + + /// Try to send data track packets over the transport. + /// + /// Packets will be sent until the first error, at which point the rest of + /// the batch will be dropped. + /// + fn try_send_data_track_packets(&self, packets: Vec) { + // TODO: handle buffering + for packet in packets { + if let Err(err) = self.inner.dt_transport.send(&packet, true) { + log::trace!("Failed to send packet: {}", err); + break; + } + } + } + pub fn e2ee_manager(&self) -> Option { self.inner.e2ee_manager.clone() } @@ -816,64 +800,6 @@ impl SessionInner { log::debug!("closing signal_task"); } - async fn local_dt_forward_task( - self: Arc, - mut events: impl Stream + Unpin, - ) { - while let Some(event) = events.next().await { - self.clone().forward_local_dt_event(event).await; - } - log::debug!("closing local_dt_forward_task"); - } - - async fn remote_dt_forward_task( - self: Arc, - mut events: impl Stream + Unpin, - ) { - while let Some(event) = events.next().await { - self.clone().forward_remote_dt_event(event).await; - } - log::debug!("closing remote_dt_forward_task"); - } - - async fn forward_local_dt_event(self: Arc, event: dt::local::OutputEvent) { - use dt::local::OutputEvent; - match event { - OutputEvent::SfuPublishRequest(event) => { - self.signal_client - .send(proto::signal_request::Message::PublishDataTrackRequest(event.into())) - .await - } - OutputEvent::SfuUnpublishRequest(event) => { - self.signal_client - .send(proto::signal_request::Message::UnpublishDataTrackRequest(event.into())) - .await - } - OutputEvent::PacketsAvailable(packets) => { - for packet in packets { - if let Err(err) = self.dt_transport.send(&packet, true) { - log::trace!("Failed to send packet over transport: {}", err); - break; // Drop the rest of the batch - } - } - } - } - } - - async fn forward_remote_dt_event(self: Arc, event: dt::remote::OutputEvent) { - use dt::remote::OutputEvent; - match event { - OutputEvent::SfuUpdateSubscription(event) => { - self.signal_client - .send(proto::signal_request::Message::UpdateDataSubscription(event.into())) - .await - } - OutputEvent::TrackAvailable(track) => { - let _ = self.emitter.send(SessionEvent::RemoteDataTrackPublished(track)); - } - } - } - async fn data_channel_task( self: Arc, mut dc_events: mpsc::UnboundedReceiver, @@ -1106,7 +1032,7 @@ impl SessionInner { &mut update, local_participant_identity, ) { - _ = self.remote_dt_input.send(event.into()); + _ = self.emitter.send(SessionEvent::RemoteDataTrackInput(event.into())); } let _ = self .emitter @@ -1142,7 +1068,7 @@ impl SessionInner { if let Some(event) = dt::local::publish_result_from_request_response(&request_response) { - _ = self.local_dt_input.send(event.into()); + _ = self.emitter.send(SessionEvent::LocalDataTrackInput(event.into())); return Ok(()); } let mut pending_requests = self.pending_requests.lock(); @@ -1152,15 +1078,15 @@ impl SessionInner { } proto::signal_response::Message::PublishDataTrackResponse(publish_res) => { let event: dt::local::SfuPublishResponse = publish_res.try_into()?; - _ = self.local_dt_input.send(event.into()); + _ = self.emitter.send(SessionEvent::LocalDataTrackInput(event.into())); } proto::signal_response::Message::UnpublishDataTrackResponse(unpublish_res) => { let event: dt::local::SfuUnpublishResponse = unpublish_res.try_into()?; - _ = self.local_dt_input.send(event.into()); + _ = self.emitter.send(SessionEvent::LocalDataTrackInput(event.into())); } proto::signal_response::Message::DataTrackSubscriberHandles(subscriber_handles) => { let event: dt::remote::SfuSubscriberHandles = subscriber_handles.try_into()?; - _ = self.remote_dt_input.send(event.into()); + _ = self.emitter.send(SessionEvent::RemoteDataTrackInput(event.into())); } proto::signal_response::Message::RefreshToken(ref token) => { let url = self.signal_client.url(); @@ -1211,7 +1137,7 @@ impl SessionInner { LOSSY_DC_LABEL => &self.sub_lossy_dc, RELIABLE_DC_LABEL => &self.sub_reliable_dc, DATA_TRACK_DC_LABEL => { - handle_remote_dt_packets(&data_channel, self.remote_dt_input.clone()); + handle_remote_dt_packets(&data_channel, self.emitter.downgrade()); &self.sub_dt_transport } _ => return Ok(()), @@ -1645,17 +1571,6 @@ impl SessionInner { Ok(()) } - pub async fn publish_data_track( - self: &Arc, - options: DataTrackOptions, - ) -> Result { - self.ensure_publisher_connected_with_dc(self.dt_transport.clone()) - .await - .inspect_err(|err| log::debug!("Data track transport not connected: {}", err)) - .map_err(|_| PublishError::Disconnected)?; // TODO: better error mapping - self.local_dt_input.publish_track(options).await - } - async fn publish_data( self: &Arc, mut packet: proto::DataPacket, @@ -1690,8 +1605,11 @@ impl SessionInner { .key_provider() .map_or(0, |kp| kp.get_latest_key_index() as u32); - match e2ee_manager.encrypt_data(payload_bytes, &self.participant_info.identity, key_index) - { + match e2ee_manager.encrypt_data( + payload_bytes, + &self.participant_info.identity, + key_index, + ) { Ok(encrypted_data) => { // Replace with EncryptedPacket variant packet.value = Some(proto::data_packet::Value::EncryptedPacket( @@ -1994,15 +1912,16 @@ impl SessionInner { } } -/// Forward remote data track packets to the remote track manager. -pub fn handle_remote_dt_packets(dc: &DataChannel, manager: dt::remote::ManagerInput) { +/// 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| { if !buffer.binary { log::error!("Received non-binary message"); return; } let packet: Bytes = buffer.data.to_vec().into(); // TODO: avoid clone if possible - _ = manager.send(packet.into()); + let Some(emitter) = emitter.upgrade() else { return }; + _ = emitter.send(SessionEvent::RemoteDataTrackInput(packet.into())); }); dc.on_message(on_message.into()); } From 843b3c9ff2457c3b6670739f9cfefa70db51f798 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:23:52 +1100 Subject: [PATCH 221/232] Ensure data channel transport is open --- livekit/src/rtc_engine/rtc_session.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 9f4e4169a..920c0eae0 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -624,6 +624,9 @@ impl RtcSession { use dt::local::OutputEvent; match event.into() { OutputEvent::SfuPublishRequest(event) => { + if let Err(err) = self.inner.ensure_data_track_publisher_connected().await { + log::error!("Failed to open data track publish transport: {}", err); + } self.signal_client() .send(proto::signal_request::Message::PublishDataTrackRequest(event.into())) .await @@ -1838,6 +1841,12 @@ impl SessionInner { Ok(()) } + /// Ensure the required data channel for publishing data track frames is open. + async fn ensure_data_track_publisher_connected(self: &Arc) -> EngineResult<()> { + self.ensure_publisher_connected_with_dc(self.dt_transport.clone()).await?; + Ok(()) + } + async fn ensure_publisher_connected_with_dc( self: &Arc, required_dc: DataChannel, From 20796623f429197ea40d70b35e5e77f3b0d7656b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:27:07 +1100 Subject: [PATCH 222/232] Additional unit tests for depacketizer Backported from JS implementation --- livekit-datatrack/src/remote/depacketizer.rs | 78 ++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/livekit-datatrack/src/remote/depacketizer.rs b/livekit-datatrack/src/remote/depacketizer.rs index 37b52f604..fc4710674 100644 --- a/livekit-datatrack/src/remote/depacketizer.rs +++ b/livekit-datatrack/src/remote/depacketizer.rs @@ -242,6 +242,7 @@ mod tests { use super::*; use fake::{Fake, Faker}; use test_case::test_case; + use crate::utils::Counter; #[test] fn test_single_packet() { @@ -336,6 +337,83 @@ mod tests { )); } + #[test] + fn test_unknown_frame() { + let mut depacketizer = Depacketizer::new(); + + let mut packet: Packet = Faker.fake(); + let frame_number = packet.header.frame_number; + packet.header.marker = FrameMarker::Inter; + // Start packet for this frame will never be pushed. + + let result = depacketizer.push(packet); + let drop = result.drop_error.unwrap(); + assert_eq!(drop.frame_number, frame_number); + assert!(matches!( + drop.reason, + DepacketizerDropReason::UnknownFrame + )); + } + + #[test] + fn test_multi_frame() { + let mut depacketizer = Depacketizer::new(); + + let mut sequence = Counter::new(0); + for frame_number in 0..10 { + let mut packet: Packet = Faker.fake(); + packet.header.frame_number = frame_number; + packet.header.marker = FrameMarker::Start; + packet.header.sequence = sequence.get_then_increment(); + + let result = depacketizer.push(packet.clone()); + assert!(result.drop_error.is_none() && result.frame.is_none()); + + packet.header.marker = FrameMarker::Inter; + packet.header.sequence = sequence.get_then_increment(); + + let result = depacketizer.push(packet.clone()); + assert!(result.drop_error.is_none() && result.frame.is_none()); + + packet.header.marker = FrameMarker::Final; + packet.header.sequence = sequence.get_then_increment(); + + let result = depacketizer.push(packet); + assert!(result.drop_error.is_none() && result.frame.is_some()); + } + } + + #[test] + fn test_duplicate_sequence_numbers() { + let mut depacketizer = Depacketizer::new(); + + let mut packet: Packet = Faker.fake(); + packet.header.marker = FrameMarker::Start; + packet.header.sequence = 1; + packet.payload = Bytes::from(vec![0xAB; 3]); + + let result = depacketizer.push(packet.clone()); + assert!(result.drop_error.is_none() && result.frame.is_none()); + + packet.header.marker = FrameMarker::Inter; + packet.header.sequence = 1; // Same sequence number + packet.payload = Bytes::from(vec![0xCD; 3]); + + let result = depacketizer.push(packet.clone()); + assert!(result.drop_error.is_none() && result.frame.is_none()); + + packet.header.marker = FrameMarker::Final; + packet.header.sequence = 2; + packet.payload = Bytes::from(vec![0xEF; 3]); + + let result = depacketizer.push(packet.clone()); + assert!(result.drop_error.is_none()); + let frame = result.frame.unwrap(); + + assert!(frame.payload.starts_with(&[0xCD; 3])); + // Should retain the second packet with duplicate sequence number + } + impl fake::Dummy for Packet { fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { let payload_len = rng.random_range(0..=1500); From 61fab999ea208e93a48fe8ece7c931b46f671211 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:22:04 +1100 Subject: [PATCH 223/232] Include published tracks in sync state --- livekit-datatrack/src/local/events.rs | 8 +++ livekit-datatrack/src/local/manager.rs | 44 ++++++++++++++- livekit-datatrack/src/local/proto.rs | 78 ++++++++++++++------------ livekit/src/room/mod.rs | 7 ++- 4 files changed, 97 insertions(+), 40 deletions(-) diff --git a/livekit-datatrack/src/local/events.rs b/livekit-datatrack/src/local/events.rs index cb4935b89..0f4621853 100644 --- a/livekit-datatrack/src/local/events.rs +++ b/livekit-datatrack/src/local/events.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::sync::Arc; use crate::{ api::{DataTrackInfo, DataTrackOptions, LocalDataTrack, PublishError}, packet::Handle, @@ -25,6 +26,7 @@ use tokio::sync::oneshot; pub enum InputEvent { PublishRequest(PublishRequest), PublishCancelled(PublishCancelled), + QueryPublished(QueryPublished), UnpublishRequest(UnpublishRequest), SfuPublishResponse(SfuPublishResponse), SfuUnpublishResponse(SfuUnpublishResponse), @@ -82,6 +84,12 @@ pub struct SfuUnpublishResponse { pub handle: Handle, } +/// Get information about all currently published tracks. +#[derive(Debug)] +pub struct QueryPublished { + pub(super) result_tx: oneshot::Sender>> +} + // MARK: - Output events /// Request sent to the SFU to publish a track. diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 37787b810..890de482c 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -87,6 +87,7 @@ impl Manager { match event { InputEvent::PublishRequest(event) => self.on_publish_request(event).await, InputEvent::PublishCancelled(event) => self.on_publish_cancelled(event).await, + InputEvent::QueryPublished(event) => self.on_query_published(event).await, InputEvent::UnpublishRequest(event) => self.on_unpublish_request(event).await, InputEvent::SfuPublishResponse(event) => self.on_sfu_publish_response(event).await, InputEvent::SfuUnpublishResponse(event) => { @@ -160,6 +161,20 @@ impl Manager { } } + async fn on_query_published(&self, event: QueryPublished) { + let published_info: Vec<_> = self + .descriptors + .iter() + .filter_map(|descriptor| { + let (_, Descriptor::Active { info, .. }) = descriptor else { + return None; + }; + info.clone().into() + }) + .collect(); + _ = event.result_tx.send(published_info); + } + async fn on_unpublish_request(&mut self, event: UnpublishRequest) { self.remove_descriptor(event.handle); @@ -207,7 +222,11 @@ impl Manager { self.descriptors.insert( info.pub_handle, - Descriptor::Active { published_tx: published_tx.clone(), task_handle }, + Descriptor::Active { + info: info.clone(), + published_tx: published_tx.clone(), + task_handle, + }, ); let inner = LocalTrackInner { frame_tx, published_tx }; @@ -237,7 +256,7 @@ impl Manager { Descriptor::Pending(result_tx) => { _ = result_tx.send(Err(PublishError::Disconnected)) } - Descriptor::Active { published_tx, task_handle } => { + Descriptor::Active { published_tx, task_handle, .. } => { _ = published_tx.send(false); task_handle.await; } @@ -304,7 +323,11 @@ enum Descriptor { /// /// The associated channel is used to end the track task. /// - Active { published_tx: watch::Sender, task_handle: livekit_runtime::JoinHandle<()> }, + Active { + info: Arc, + published_tx: watch::Sender, + task_handle: livekit_runtime::JoinHandle<()>, + }, } /// Channel for sending [`InputEvent`]s to [`Manager`]. @@ -337,6 +360,21 @@ impl ManagerInput { Ok(track) } + /// Get information about all currently published tracks. + /// + /// This does not include publications that are still pending. + /// + pub async fn published_tracks(&self) -> Vec> { + let (result_tx, result_rx) = oneshot::channel(); + + let event = QueryPublished { result_tx }; + if self.event_in_tx.send(event.into()).await.is_err() { + return vec![]; + } + + result_rx.await.unwrap_or_default() + } + /// How long to wait for before timeout. const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); } diff --git a/livekit-datatrack/src/local/proto.rs b/livekit-datatrack/src/local/proto.rs index 6ed7a6ab9..0e78a4728 100644 --- a/livekit-datatrack/src/local/proto.rs +++ b/livekit-datatrack/src/local/proto.rs @@ -19,7 +19,7 @@ use crate::{ }; use anyhow::{anyhow, Context}; use livekit_protocol as proto; -use std::mem; +use std::borrow::Borrow; // MARK: - Output event -> protocol @@ -91,18 +91,38 @@ pub fn publish_result_from_request_response( Some(event) } -pub fn publish_results_from_sync_state( - msg: &mut proto::SyncState, -) -> Result, InternalError> { - mem::take(&mut msg.publish_data_tracks) +// MARK: - Sync state support + +impl From for proto::DataTrackInfo { + fn from(info: DataTrackInfo) -> Self { + let encryption = if info.uses_e2ee() { + proto::encryption::Type::Gcm + } else { + proto::encryption::Type::None + }; + Self { + pub_handle: info.pub_handle.into(), + sid: info.sid().to_string(), + name: info.name, + encryption: encryption as i32, + } + } +} + +/// Form publish responses for each publish data track to support sync state. +pub fn publish_responses_for_sync_state( + published_tracks: impl IntoIterator>, +) -> Vec { + published_tracks .into_iter() - .map(TryInto::::try_into) - .collect::, InternalError>>() + .map(|info| proto::PublishDataTrackResponse { info: Some(info.borrow().clone().into()) }) + .collect() } #[cfg(test)] mod tests { use super::*; + use fake::{Fake, Faker}; #[test] fn test_from_publish_request_event() { @@ -164,33 +184,21 @@ mod tests { } #[test] - fn test_from_sync_state() { - let mut sync_state = proto::SyncState { - publish_data_tracks: vec![ - proto::PublishDataTrackResponse { - info: proto::DataTrackInfo { - pub_handle: 1, - sid: "DTR_1234".into(), - name: "track1".into(), - encryption: proto::encryption::Type::Gcm.into(), - } - .into(), - }, - proto::PublishDataTrackResponse { - info: proto::DataTrackInfo { - pub_handle: 2, - sid: "DTR_4567".into(), - name: "track2".into(), - encryption: proto::encryption::Type::Gcm.into(), - } - .into(), - }, - ], - ..Default::default() - }; - let events = publish_results_from_sync_state(&mut sync_state).expect("Expected events"); - assert_eq!(events.len(), 2); - assert!(sync_state.publish_data_tracks.is_empty(), "Expected original vec taken"); - assert!(events.into_iter().all(|event| event.result.is_ok())); + fn test_publish_responses_for_sync_state() { + let mut first: DataTrackInfo = Faker.fake(); + first.uses_e2ee = true; + + let mut second: DataTrackInfo = Faker.fake(); + second.uses_e2ee = false; + + let publish_responses = publish_responses_for_sync_state(vec![first, second]); + assert_eq!( + publish_responses[0].info.as_ref().unwrap().encryption(), + proto::encryption::Type::Gcm + ); + assert_eq!( + publish_responses[1].info.as_ref().unwrap().encryption(), + proto::encryption::Type::None + ); } } diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index bafa35de9..4df661e8f 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -1244,6 +1244,10 @@ impl RoomSession { }); } + let publish_data_tracks = dt::local::publish_responses_for_sync_state( + self.local_dt_input.published_tracks().await, + ); + let sync_state = proto::SyncState { answer: Some(proto::SessionDescription { sdp: answer.to_string(), @@ -1266,8 +1270,7 @@ impl RoomSession { publish_tracks: self.local_participant.published_tracks_info(), data_channels: dcs, datachannel_receive_states: session.data_channel_receive_states(), - publish_data_tracks: vec![], - // TODO: handle sync state + publish_data_tracks }; log::debug!("sending sync state {:?}", sync_state); From 9219deccef4f979693d4b0a0e0dd3cf196540121 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:51:12 +1100 Subject: [PATCH 224/232] Reduce E2E test boilerplate --- livekit/tests/data_track_test.rs | 80 ++++++++------------------------ 1 file changed, 20 insertions(+), 60 deletions(-) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 80fe97d3b..c29af49a2 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -44,7 +44,7 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { let mut rooms = test_rooms(2).await?; let (pub_room, _) = rooms.pop().unwrap(); - let (sub_room, mut sub_room_event_rx) = rooms.pop().unwrap(); + let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); let pub_identity = pub_room.local_participant().identity(); let frame_count = (PUBLISH_DURATION.as_secs_f64() * publish_fps).round() as u64; @@ -67,16 +67,7 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { }; let subscribe = async move { - let track = async move { - while let Some(event) = sub_room_event_rx.recv().await { - let RoomEvent::RemoteDataTrackPublished(track) = event else { - continue; - }; - return Ok(track); - } - Err(anyhow!("No track published")) - } - .await?; + let track = wait_for_remote_track(&mut sub_room_event_rx).await?; log::info!("Got remote track: {}", track.info().sid()); assert!(track.is_published()); @@ -86,19 +77,13 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { let mut subscription = track.subscribe().await?; - //sub_room.simulate_scenario(livekit::SimulateScenario::SignalReconnect).await.unwrap(); - let mut recv_count = 0; - while let Some(frame) = subscription.next().await { let payload = frame.payload(); if let Some(first_byte) = payload.first() { assert!(payload.iter().all(|byte| byte == first_byte)); } assert_eq!(frame.user_timestamp(), None); - // if recv_count == 100 { - // sub_room.simulate_scenario(livekit::SimulateScenario::SignalReconnect).await.unwrap(); - // } recv_count += 1; } @@ -110,7 +95,7 @@ async fn test_data_track(publish_fps: f64, payload_len: usize) -> Result<()> { } Ok(()) }; - timeout(PUBLISH_DURATION + Duration::from_secs(5), async { try_join!(publish, subscribe) }) + timeout(PUBLISH_DURATION + Duration::from_secs(25), async { try_join!(publish, subscribe) }) .await??; Ok(()) } @@ -230,16 +215,7 @@ async fn test_e2ee() -> Result<()> { }; let subscribe = async move { - let track = async move { - while let Some(event) = sub_room_event_rx.recv().await { - let RoomEvent::RemoteDataTrackPublished(track) = event else { - continue; - }; - return Ok(track); - } - Err(anyhow!("No track published")) - } - .await?; + let track = wait_for_remote_track(&mut sub_room_event_rx).await?; assert!(track.info().uses_e2ee()); let mut subscription = track.subscribe().await?; @@ -278,16 +254,7 @@ async fn test_published_state() -> Result<()> { }; let subscribe = async move { - let track = async move { - while let Some(event) = sub_room_event_rx.recv().await { - let RoomEvent::RemoteDataTrackPublished(track) = event else { - continue; - }; - return Ok(track); - } - Err(anyhow!("No track published")) - } - .await?; + let track = wait_for_remote_track(&mut sub_room_event_rx).await?; assert!(track.is_published()); let elapsed = { @@ -324,17 +291,7 @@ async fn test_resubscribe() -> Result<()> { }; let subscribe = async move { - let track = async move { - while let Some(event) = sub_room_event_rx.recv().await { - let RoomEvent::RemoteDataTrackPublished(track) = event else { - continue; - }; - return Ok(track); - } - Err(anyhow!("No track published")) - } - .await - .unwrap(); + let track = wait_for_remote_track(&mut sub_room_event_rx).await.unwrap(); let mut successful_subscriptions = 0; for _ in 0..ITERATIONS { @@ -378,17 +335,7 @@ async fn test_frame_with_user_timestamp() -> Result<()> { }; let subscribe = async move { - let track = async move { - while let Some(event) = sub_room_event_rx.recv().await { - let RoomEvent::RemoteDataTrackPublished(track) = event else { - continue; - }; - return Ok(track); - } - Err(anyhow!("No track published")) - } - .await - .unwrap(); + let track = wait_for_remote_track(&mut sub_room_event_rx).await.unwrap(); let mut stream = track.subscribe().await.unwrap(); while let Some(frame) = stream.next().await { @@ -405,4 +352,17 @@ async fn test_frame_with_user_timestamp() -> Result<()> { }) .await?; Ok(()) +} + +/// Waits for the first remote data track to be published. +#[cfg(feature = "__lk-e2e-test")] +async fn wait_for_remote_track( + rx: &mut tokio::sync::mpsc::UnboundedReceiver, +) -> Result { + while let Some(event) = rx.recv().await { + if let RoomEvent::RemoteDataTrackPublished(track) = event { + return Ok(track); + } + } + Err(anyhow!("No track published")) } \ No newline at end of file From f81791a596947583d8e90442a1d12facec1f11d6 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 31 Jan 2026 11:12:54 +1100 Subject: [PATCH 225/232] Docs --- livekit-datatrack/src/remote/proto.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index 19d91f610..84dbeb986 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -39,7 +39,7 @@ impl TryFrom for SfuSubscriberHandles { } } -/// Extracts a [`PublicationsUpdatedEvent`] from a join response. +/// Extracts a [`SfuPublicationUpdates`] from a join response. /// /// This takes ownership of the `data_tracks` vector for each participant /// (except for the local participant), leaving an empty vector in its place. @@ -50,7 +50,7 @@ pub fn event_from_join( event_from_participant_info(&mut msg.other_participants, None) } -/// Extracts a [`PublicationsUpdatedEvent`] from a participant update. +/// Extracts a [`SfuPublicationUpdates`] from a participant update. /// /// This takes ownership of the `data_tracks` vector for each participant in /// the update, leaving an empty vector in its place. @@ -63,7 +63,7 @@ pub fn event_from_participant_update( event_from_participant_info(&mut msg.participants, local_participant_identity.into()) } -/// Extracts a [`PublicationsUpdatedEvent`] from a participant info list. +/// Extracts a [`SfuPublicationUpdates`] from a participant info list. /// /// Tracks published by the local participant will be filtered out if the local /// participant identity is set. From 67d56e1a50df827c4384acbb2842df0db072bfdf Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 31 Jan 2026 12:54:03 +1100 Subject: [PATCH 226/232] Test subscriber side fault, fix full reconnect --- livekit-datatrack/src/remote/events.rs | 6 +++ livekit-datatrack/src/remote/manager.rs | 42 +++++++++++++---- livekit/src/room/mod.rs | 5 +- livekit/tests/data_track_test.rs | 61 ++++++++++++++++++++++--- 4 files changed, 99 insertions(+), 15 deletions(-) diff --git a/livekit-datatrack/src/remote/events.rs b/livekit-datatrack/src/remote/events.rs index 9e7f4175d..102e45203 100644 --- a/livekit-datatrack/src/remote/events.rs +++ b/livekit-datatrack/src/remote/events.rs @@ -30,6 +30,12 @@ pub enum InputEvent { SfuSubscriberHandles(SfuSubscriberHandles), /// Packet has been received over the transport. PacketReceived(Bytes), + /// Resend all subscription updates. + /// + /// This must be sent after a full reconnect to ensure the SFU knows which + /// tracks are subscribed to locally. + /// + ResendSubscriptionUpdates, /// Shutdown the manager, ending any subscriptions. Shutdown, } diff --git a/livekit-datatrack/src/remote/manager.rs b/livekit-datatrack/src/remote/manager.rs index f333b87aa..11ef250b9 100644 --- a/livekit-datatrack/src/remote/manager.rs +++ b/livekit-datatrack/src/remote/manager.rs @@ -103,6 +103,9 @@ impl Manager { } InputEvent::SfuSubscriberHandles(event) => self.on_sfu_subscriber_handles(event), InputEvent::PacketReceived(bytes) => self.on_packet_received(bytes), + InputEvent::ResendSubscriptionUpdates => { + self.on_resend_subscription_updates().await + } InputEvent::Shutdown => break, } } @@ -221,21 +224,27 @@ impl Manager { } } - fn register_subscriber_handle(&mut self, sub_handle: Handle, sid: DataTrackSid) { + fn register_subscriber_handle(&mut self, assigned_handle: Handle, sid: DataTrackSid) { let Some(descriptor) = self.descriptors.get_mut(&sid) else { log::warn!("Unknown track: {}", sid); return; }; let result_txs = match &mut descriptor.subscription { - SubscriptionState::Pending { result_txs } => mem::take(result_txs), SubscriptionState::None => { - log::warn!("No subscription"); + // Handle assigned when there is no pending or active subscription is unexpected. + log::warn!("No subscription for {}", sid); return; } - SubscriptionState::Active { .. } => { - log::warn!("Cannot change existing handle"); + SubscriptionState::Active { sub_handle, .. } => { + // Update handle for an active subscription. This can occur following a full reconnect. + *sub_handle = assigned_handle; + self.sub_handles.insert(assigned_handle, sid); return; } + SubscriptionState::Pending { result_txs } => { + // Handle assigned for pending subscription, transition to active. + mem::take(result_txs) + } }; let (packet_tx, packet_rx) = mpsc::channel(4); // TODO: tune @@ -264,9 +273,13 @@ impl Manager { }; let task_handle = livekit_runtime::spawn(track_task.run()); - descriptor.subscription = - SubscriptionState::Active { sub_handle, packet_tx, frame_tx, task_handle }; - self.sub_handles.insert(sub_handle, sid); + descriptor.subscription = SubscriptionState::Active { + sub_handle: assigned_handle, + packet_tx, + frame_tx, + task_handle, + }; + self.sub_handles.insert(assigned_handle, sid); for result_tx in result_txs { _ = result_tx.send(Ok(frame_rx.resubscribe())); @@ -298,6 +311,19 @@ impl Manager { .inspect_err(|err| log::debug!("Cannot send packet to track pipeline: {}", err)); } + async fn on_resend_subscription_updates(&self) { + let update_events = + self.descriptors.iter().filter_map(|(sid, descriptor)| match descriptor.subscription { + SubscriptionState::None => None, + SubscriptionState::Pending { .. } | SubscriptionState::Active { .. } => { + Some(SfuUpdateSubscription { sid: sid.clone(), subscribe: true }) + } + }); + for event in update_events { + _ = self.event_out_tx.send(event.into()).await; + } + } + /// Performs cleanup before the task ends. async fn shutdown(self) { for (_, descriptor) in self.descriptors { diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index 4df661e8f..04e2bb6ee 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -1270,7 +1270,7 @@ impl RoomSession { publish_tracks: self.local_participant.published_tracks_info(), data_channels: dcs, datachannel_receive_states: session.data_channel_receive_states(), - publish_data_tracks + publish_data_tracks, }; log::debug!("sending sync state {:?}", sync_state); @@ -1399,6 +1399,9 @@ impl RoomSession { fn handle_restarted(self: &Arc, tx: oneshot::Sender<()>) { let _ = tx.send(()); + // Ensure SFU continues delivering packets for existing data track subscriptions. + _ = self.remote_dt_input.send(dt::remote::InputEvent::ResendSubscriptionUpdates); + // Unpublish and republish every track // At this time we know that the RtcSession is successfully restarted let published_tracks = self.local_participant.track_publications(); diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index c29af49a2..01eb329a1 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -17,7 +17,7 @@ use { anyhow::{anyhow, Ok, Result}, common::{test_rooms, test_rooms_with_options, TestRoomOptions}, futures_util::StreamExt, - livekit::prelude::*, + livekit::{prelude::*, SimulateScenario}, livekit_api::access_token::VideoGrants, std::time::{Duration, Instant}, test_case::test_case, @@ -262,7 +262,7 @@ async fn test_published_state() -> Result<()> { track.wait_for_unpublish().await; start.elapsed() }; - assert!(elapsed.abs_diff(PUBLISH_DURATION) <= Duration::from_millis(20),); + assert!(elapsed.abs_diff(PUBLISH_DURATION) <= Duration::from_millis(20)); assert!(!track.is_published()); Ok(()) @@ -318,7 +318,6 @@ async fn test_resubscribe() -> Result<()> { #[cfg(feature = "__lk-e2e-test")] #[test_log::test(tokio::test)] async fn test_frame_with_user_timestamp() -> Result<()> { - let mut rooms = test_rooms(2).await?; let (pub_room, _) = rooms.pop().unwrap(); @@ -327,8 +326,7 @@ async fn test_frame_with_user_timestamp() -> Result<()> { let publish = async move { let track = pub_room.local_participant().publish_data_track("my_track").await.unwrap(); loop { - let frame = DataTrackFrame::new(vec![0xFA; 64]) - .with_user_timestamp_now(); + let frame = DataTrackFrame::new(vec![0xFA; 64]).with_user_timestamp_now(); _ = track.try_push(frame); time::sleep(Duration::from_millis(50)).await; } @@ -338,13 +336,18 @@ async fn test_frame_with_user_timestamp() -> Result<()> { let track = wait_for_remote_track(&mut sub_room_event_rx).await.unwrap(); let mut stream = track.subscribe().await.unwrap(); + let mut got_frame = false; while let Some(frame) = stream.next().await { // Ensure we can at least get one frame. assert!(!frame.payload().is_empty()); let duration = frame.duration_since_timestamp().expect("Missing timestamp"); assert!(duration.as_millis() < 1000); + got_frame = true; break; } + if !got_frame { + panic!("No frame received"); + } }; let _ = timeout(Duration::from_secs(5), async { @@ -354,6 +357,52 @@ async fn test_frame_with_user_timestamp() -> Result<()> { Ok(()) } +#[cfg(feature = "__lk-e2e-test")] +#[test_case(SimulateScenario::SignalReconnect; "signal_reconnect")] +#[test_case(SimulateScenario::ForceTcp; "full_reconnect")] +#[test_log::test(tokio::test)] +async fn test_subscriber_side_fault(scenario: SimulateScenario) -> Result<()> { + let mut rooms = test_rooms(2).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (sub_room, mut sub_room_event_rx) = rooms.pop().unwrap(); + + let publish = async move { + let track = pub_room.local_participant().publish_data_track("my_track").await.unwrap(); + loop { + _ = track.try_push(vec![0xFA; 64].into()); + time::sleep(Duration::from_millis(50)).await; + } + }; + + let subscribe = async move { + let track = wait_for_remote_track(&mut sub_room_event_rx).await.unwrap(); + let mut stream = track.subscribe().await.unwrap(); + + // TODO: this should also evaluate what happens if a track subscription is removed + // during a full reconnect event. + sub_room.simulate_scenario(scenario).await.unwrap(); + assert!(track.is_published()); + + let mut got_frame = false; + while let Some(frame) = stream.next().await { + // Ensure we can at least get one frame. + assert!(!frame.payload().is_empty()); + got_frame = true; + break; + } + if !got_frame { + panic!("No frame received"); + } + }; + + let _ = timeout(Duration::from_secs(15), async { + tokio::select! { _ = publish => (), _ = subscribe => () }; + }) + .await?; + Ok(()) +} + /// Waits for the first remote data track to be published. #[cfg(feature = "__lk-e2e-test")] async fn wait_for_remote_track( @@ -365,4 +414,4 @@ async fn wait_for_remote_track( } } Err(anyhow!("No track published")) -} \ No newline at end of file +} From 3f77ce926f635ae2c354a7a84fa95c058f39d7fa Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 31 Jan 2026 13:06:29 +1100 Subject: [PATCH 227/232] Test publisher side fault --- livekit/tests/data_track_test.rs | 47 ++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/livekit/tests/data_track_test.rs b/livekit/tests/data_track_test.rs index 01eb329a1..b9424d4db 100644 --- a/livekit/tests/data_track_test.rs +++ b/livekit/tests/data_track_test.rs @@ -403,6 +403,53 @@ async fn test_subscriber_side_fault(scenario: SimulateScenario) -> Result<()> { Ok(()) } +#[cfg(feature = "__lk-e2e-test")] +#[test_case(SimulateScenario::SignalReconnect; "signal_reconnect")] +#[test_case(SimulateScenario::ForceTcp; "full_reconnect")] +#[test_log::test(tokio::test)] +async fn test_publisher_side_fault(scenario: SimulateScenario) -> Result<()> { + let mut rooms = test_rooms(2).await?; + + let (pub_room, _) = rooms.pop().unwrap(); + let (_, mut sub_room_event_rx) = rooms.pop().unwrap(); + + let publish = async move { + let track = pub_room.local_participant().publish_data_track("my_track").await.unwrap(); + + pub_room.simulate_scenario(scenario).await.unwrap(); + assert!(track.is_published()); + + loop { + track + .try_push(vec![0xFA; 64].into()) + .expect("Should be able to push frame after reconnect"); + time::sleep(Duration::from_millis(50)).await; + } + }; + + let subscribe = async move { + let track = wait_for_remote_track(&mut sub_room_event_rx).await.unwrap(); + let mut stream = track.subscribe().await.unwrap(); + + let mut got_frame = false; + while let Some(frame) = stream.next().await { + // Ensure we can at least get one frame. + assert!(!frame.payload().is_empty()); + got_frame = true; + break; + } + if !got_frame { + panic!("No frame received"); + } + }; + + let _ = timeout(Duration::from_secs(10), async { + tokio::select! { _ = publish => (), _ = subscribe => () }; + }) + .await?; + Ok(()) +} + /// Waits for the first remote data track to be published. #[cfg(feature = "__lk-e2e-test")] async fn wait_for_remote_track( From 7f2724c492568e54934613ddab04f1df97f4ac31 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:09:57 +1100 Subject: [PATCH 228/232] Improve internal docs & organization --- livekit-datatrack/README.md | 6 ++-- livekit-datatrack/src/e2ee.rs | 16 +++++++--- livekit-datatrack/src/lib.rs | 11 +++++-- livekit-datatrack/src/local/events.rs | 43 ++++++++++++++++++++------ livekit-datatrack/src/local/manager.rs | 2 +- livekit-datatrack/src/remote/events.rs | 42 ++++++++++++++++++++----- livekit-datatrack/src/remote/proto.rs | 19 +++++------- livekit/src/room/e2ee/data_track.rs | 2 +- livekit/src/room/mod.rs | 4 +-- livekit/src/rtc_engine/mod.rs | 2 +- livekit/src/rtc_engine/rtc_session.rs | 2 +- 11 files changed, 106 insertions(+), 43 deletions(-) diff --git a/livekit-datatrack/README.md b/livekit-datatrack/README.md index 88c1938a2..781b5457d 100644 --- a/livekit-datatrack/README.md +++ b/livekit-datatrack/README.md @@ -1,4 +1,6 @@ -# Livekit Data Track +# LiveKit Data Track -This is an internal crate used by the LiveKit Rust SDK. +**Important**: +This is an internal crate that powers the data tracks feature in LiveKit client SDKs (including [Rust](https://crates.io/crates/livekit) and others) and is not usable directly. +To use data tracks in your application, please use the public APIs provided by the client SDKs. diff --git a/livekit-datatrack/src/e2ee.rs b/livekit-datatrack/src/e2ee.rs index 0e21a607a..1f52c40ad 100644 --- a/livekit-datatrack/src/e2ee.rs +++ b/livekit-datatrack/src/e2ee.rs @@ -26,21 +26,29 @@ pub struct EncryptedPayload { pub key_index: u8, } +/// An error indicating a payload could not be encrypted. #[derive(Debug, Error)] #[error("Encryption failed")] pub struct EncryptionError; +/// An error indicating a payload could not be decrypted. +#[derive(Debug, Error)] +#[error("Decryption failed")] +pub struct DecryptionError; + +/// Provider for encrypting payloads for E2EE. pub trait EncryptionProvider: Send + Sync + Debug { /// Encrypts the given payload being sent by the local participant. fn encrypt(&self, payload: Bytes) -> Result; } -#[derive(Debug, Error)] -#[error("Decryption failed")] -pub struct DecryptionError; - +/// Provider for decrypting payloads for E2EE. pub trait DecryptionProvider: Send + Sync + Debug { /// Decrypts the given payload received from a remote participant. + /// + /// Sender identity is required in order for the proper key to be used + /// for decryption. + /// fn decrypt( &self, payload: EncryptedPayload, diff --git a/livekit-datatrack/src/lib.rs b/livekit-datatrack/src/lib.rs index 86baf3004..573f7e11a 100644 --- a/livekit-datatrack/src/lib.rs +++ b/livekit-datatrack/src/lib.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![doc = include_str!("../README.md")] + /// Common types for local and remote tracks. mod track; @@ -36,18 +38,21 @@ mod utils; /// Internal error. mod error; -/// Public APIs re-exported by the LiveKit crate. +/// Public APIs re-exported by client SDKs. pub mod api { pub use crate::{error::*, frame::*, local::*, remote::*, track::*}; } -/// Internal APIs for use within the LiveKit crate. -pub mod internal { +/// Internal APIs used within client SDKs to power data tracks functionality. +pub mod backend { pub use crate::e2ee::*; + /// Local track publication pub mod local { pub use crate::local::{events::*, manager::*, proto::*}; } + + /// Remote track subscription pub mod remote { pub use crate::remote::{events::*, manager::*, proto::*}; } diff --git a/livekit-datatrack/src/local/events.rs b/livekit-datatrack/src/local/events.rs index 0f4621853..01e487fd4 100644 --- a/livekit-datatrack/src/local/events.rs +++ b/livekit-datatrack/src/local/events.rs @@ -21,7 +21,7 @@ use bytes::Bytes; use from_variants::FromVariants; use tokio::sync::oneshot; -/// An external event handled by [`super::manager::Manager`]. +/// An external event handled by [`Manager`](super::manager::Manager). #[derive(Debug, FromVariants)] pub enum InputEvent { PublishRequest(PublishRequest), @@ -34,7 +34,7 @@ pub enum InputEvent { Shutdown, } -/// An event produced by [`super::manager::Manager`] requiring external action. +/// An event produced by [`Manager`](super::manager::Manager) requiring external action. #[derive(Debug, FromVariants)] pub enum OutputEvent { SfuPublishRequest(SfuPublishRequest), @@ -46,6 +46,11 @@ pub enum OutputEvent { // MARK: - Input events /// Client requested to publish a track. +/// +/// Send using [`ManagerInput::publish_track`] and await the result. +/// +/// [`ManagerInput::publish_track`]: super::manager::ManagerInput::publish_track +/// #[derive(Debug)] pub struct PublishRequest { /// Publish options. @@ -54,21 +59,36 @@ pub struct PublishRequest { pub(super) result_tx: oneshot::Sender>, } -/// Client request to publish a track has been cancelled. +/// Client request to publish a track has been cancelled (internal). #[derive(Debug)] pub struct PublishCancelled { /// Publisher handle of the pending publication. pub(super) handle: Handle, } -/// Client request to unpublish a track. +/// Client request to unpublish a track (internal). #[derive(Debug)] pub struct UnpublishRequest { /// Publisher handle of the track to unpublish. pub(super) handle: Handle, } +/// Get information about all currently published tracks. +/// +/// Send using [`ManagerInput::query_tracks`] and await the result. This is used +/// to support sync state. +/// +/// [`ManagerInput::query_tracks`]: super::manager::ManagerInput::query_tracks +/// +#[derive(Debug)] +pub struct QueryPublished { + pub(super) result_tx: oneshot::Sender>> +} + /// SFU responded to a request to publish a data track. +/// +/// Protocol equivalent: [`livekit_protocol::PublishDataTrackResponse`]. +/// #[derive(Debug)] pub struct SfuPublishResponse { /// Publisher handle of the track. @@ -78,21 +98,21 @@ pub struct SfuPublishResponse { } /// SFU notification that a track has been unpublished. +/// +/// Protocol equivalent: [`livekit_protocol::UnpublishDataTrackResponse`]. +/// #[derive(Debug)] pub struct SfuUnpublishResponse { /// Publisher handle of the track that was unpublished. pub handle: Handle, } -/// Get information about all currently published tracks. -#[derive(Debug)] -pub struct QueryPublished { - pub(super) result_tx: oneshot::Sender>> -} - // MARK: - Output events /// Request sent to the SFU to publish a track. +/// +/// Protocol equivalent: [`livekit_protocol::PublishDataTrackRequest`]. +/// #[derive(Debug)] pub struct SfuPublishRequest { pub handle: Handle, @@ -101,6 +121,9 @@ pub struct SfuPublishRequest { } /// Request sent to the SFU to unpublish a track. +/// +/// Protocol equivalent: [`livekit_protocol::UnpublishDataTrackRequest`]. +/// #[derive(Debug)] pub struct SfuUnpublishRequest { /// Publisher handle of the track to unpublish. diff --git a/livekit-datatrack/src/local/manager.rs b/livekit-datatrack/src/local/manager.rs index 890de482c..6cb7f6517 100644 --- a/livekit-datatrack/src/local/manager.rs +++ b/livekit-datatrack/src/local/manager.rs @@ -364,7 +364,7 @@ impl ManagerInput { /// /// This does not include publications that are still pending. /// - pub async fn published_tracks(&self) -> Vec> { + pub async fn query_tracks(&self) -> Vec> { let (result_tx, result_rx) = oneshot::channel(); let event = QueryPublished { result_tx }; diff --git a/livekit-datatrack/src/remote/events.rs b/livekit-datatrack/src/remote/events.rs index 102e45203..d5fae9be5 100644 --- a/livekit-datatrack/src/remote/events.rs +++ b/livekit-datatrack/src/remote/events.rs @@ -21,7 +21,7 @@ use from_variants::FromVariants; use std::collections::HashMap; use tokio::sync::{broadcast, oneshot}; -/// An external event handled by [`super::manager::Manager`]. +/// An external event handled by [`Manager`](super::manager::Manager). #[derive(Debug, FromVariants)] pub enum InputEvent { SubscribeRequest(SubscribeRequest), @@ -40,12 +40,16 @@ pub enum InputEvent { Shutdown, } -/// An event produced by [`super::manager::Manager`] requiring external action. +/// An event produced by [`Manager`](super::manager::Manager) requiring external action. #[derive(Debug, FromVariants)] pub enum OutputEvent { SfuUpdateSubscription(SfuUpdateSubscription), - /// Remote track has been published and a track object has been created for - /// the user to interact with. + /// A track has been published by a remote participant and is available to be + /// subscribed to. + /// + /// Emit a public event to deliver the track to the user, allowing them to subscribe + /// with [`RemoteDataTrack::subscribe`] if desired. + /// TrackAvailable(RemoteDataTrack), } @@ -55,6 +59,13 @@ pub enum OutputEvent { pub(super) type SubscribeResult = Result, SubscribeError>; /// Client requested to subscribe to a data track. +/// +/// This is sent when the user calls [`RemoteDataTrack::subscribe`]. +/// +/// Only the first request to subscribe to a given track incurs meaningful overhead; subsequent +/// requests simply attach an additional receiver to the broadcast channel, allowing them to consume +/// frames from the existing subscription pipeline. +/// #[derive(Debug)] pub struct SubscribeRequest { /// Identifier of the track. @@ -70,15 +81,29 @@ pub struct UnsubscribeRequest { pub(super) sid: DataTrackSid, } -/// SFU notification that remote participants have published or unpublished -/// unpublished data tracks. +/// SFU notification that track publications have changed. +/// +/// This event is produced from both [`livekit_protocol::JoinResponse`] and [`livekit_protocol::ParticipantUpdate`] +/// to provide a complete view of remote participants' track publications: +/// +/// - From a `JoinResponse`, it captures the initial set of tracks published when a participant joins. +/// - From a `ParticipantUpdate`, it captures subsequent changes (i.e., new tracks being +/// published and existing tracks unpublished). +/// +/// See [`event_from_join`](super::proto::event_from_join) and +/// [`event_from_participant_update`](super::proto::event_from_participant_update). +/// #[derive(Debug)] pub struct SfuPublicationUpdates { - /// Mapping between participant identity and data tracks published by that participant. + /// Mapping between participant identity and data tracks currently + /// published by that participant. pub updates: HashMap>, } /// SFU notification that handles have been assigned for requested subscriptions. +/// +/// Protocol equivalent: [`livekit_protocol::DataTrackSubscriberHandles`]. +/// #[derive(Debug)] pub struct SfuSubscriberHandles { /// Mapping between track handles attached to incoming packets to the @@ -89,6 +114,9 @@ pub struct SfuSubscriberHandles { // MARK: - Output events /// Request sent to the SFU to update the subscription for a data track. +/// +/// Protocol equivalent: [`livekit_protocol::UpdateDataSubscription`]. +/// #[derive(Debug)] pub struct SfuUpdateSubscription { /// Identifier of the affected track. diff --git a/livekit-datatrack/src/remote/proto.rs b/livekit-datatrack/src/remote/proto.rs index 84dbeb986..c3c505bc6 100644 --- a/livekit-datatrack/src/remote/proto.rs +++ b/livekit-datatrack/src/remote/proto.rs @@ -17,7 +17,7 @@ use crate::{ api::{DataTrackInfo, DataTrackSid, InternalError}, packet::Handle, }; -use livekit_protocol::{self as proto, ParticipantInfo}; +use livekit_protocol as proto; use std::{collections::HashMap, mem}; // MARK: - Protocol -> input event @@ -39,7 +39,7 @@ impl TryFrom for SfuSubscriberHandles { } } -/// Extracts a [`SfuPublicationUpdates`] from a join response. +/// Extracts an [`SfuPublicationUpdates`] event from a join response. /// /// This takes ownership of the `data_tracks` vector for each participant /// (except for the local participant), leaving an empty vector in its place. @@ -50,7 +50,7 @@ pub fn event_from_join( event_from_participant_info(&mut msg.other_participants, None) } -/// Extracts a [`SfuPublicationUpdates`] from a participant update. +/// Extracts an [`SfuPublicationUpdates`] event from a participant update. /// /// This takes ownership of the `data_tracks` vector for each participant in /// the update, leaving an empty vector in its place. @@ -63,13 +63,8 @@ pub fn event_from_participant_update( event_from_participant_info(&mut msg.participants, local_participant_identity.into()) } -/// Extracts a [`SfuPublicationUpdates`] from a participant info list. -/// -/// Tracks published by the local participant will be filtered out if the local -/// participant identity is set. -/// fn event_from_participant_info( - msg: &mut [ParticipantInfo], + msg: &mut [proto::ParticipantInfo], local_participant_identity: Option<&str>, ) -> Result { let updates = msg @@ -84,7 +79,9 @@ fn event_from_participant_info( Ok(SfuPublicationUpdates { updates }) } -fn extract_track_info(msg: &mut ParticipantInfo) -> Result, InternalError> { +fn extract_track_info( + msg: &mut proto::ParticipantInfo, +) -> Result, InternalError> { mem::take(&mut msg.data_tracks) .into_iter() .map(TryInto::::try_into) @@ -98,7 +95,7 @@ impl From for proto::UpdateDataSubscription { let update = proto::update_data_subscription::Update { track_sid: event.sid.into(), subscribe: event.subscribe, - options: Default::default(), // TODO: pass through options + options: Default::default(), }; Self { updates: vec![update] } } diff --git a/livekit/src/room/e2ee/data_track.rs b/livekit/src/room/e2ee/data_track.rs index 8c86c0823..7f1bbf877 100644 --- a/livekit/src/room/e2ee/data_track.rs +++ b/livekit/src/room/e2ee/data_track.rs @@ -14,7 +14,7 @@ use crate::{id::ParticipantIdentity, E2eeManager}; use bytes::Bytes; -use livekit_datatrack::internal as dt; +use livekit_datatrack::backend as dt; /// Wrapper around [`E2eeManager`] implementing [`dt::EncryptionProvider`]. #[derive(Debug)] diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index 04e2bb6ee..f6357da92 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -24,7 +24,7 @@ use libwebrtc::{ RtcError, }; use livekit_api::signal_client::{SignalOptions, SignalSdkOptions}; -use livekit_datatrack::{api::RemoteDataTrack, internal as dt}; +use livekit_datatrack::{api::RemoteDataTrack, backend as dt}; use livekit_protocol::observer::Dispatcher; use livekit_protocol::{self as proto, encryption}; use livekit_runtime::JoinHandle; @@ -1245,7 +1245,7 @@ impl RoomSession { } let publish_data_tracks = dt::local::publish_responses_for_sync_state( - self.local_dt_input.published_tracks().await, + self.local_dt_input.query_tracks().await, ); let sync_state = proto::SyncState { diff --git a/livekit/src/rtc_engine/mod.rs b/livekit/src/rtc_engine/mod.rs index 20694dfda..d3539a95f 100644 --- a/livekit/src/rtc_engine/mod.rs +++ b/livekit/src/rtc_engine/mod.rs @@ -14,7 +14,7 @@ use libwebrtc::prelude::*; use livekit_api::signal_client::{SignalError, SignalOptions}; -use livekit_datatrack::internal as dt; +use livekit_datatrack::backend as dt; use livekit_protocol as proto; use livekit_runtime::{interval, Interval, JoinHandle}; use parking_lot::{RwLock, RwLockReadGuard}; diff --git a/livekit/src/rtc_engine/rtc_session.rs b/livekit/src/rtc_engine/rtc_session.rs index 920c0eae0..612824540 100644 --- a/livekit/src/rtc_engine/rtc_session.rs +++ b/livekit/src/rtc_engine/rtc_session.rs @@ -26,7 +26,7 @@ use std::{ use bytes::Bytes; use libwebrtc::{prelude::*, stats::RtcStats}; use livekit_api::signal_client::{SignalClient, SignalEvent, SignalEvents}; -use livekit_datatrack::internal as dt; +use livekit_datatrack::backend as dt; use livekit_protocol::{self as proto}; use livekit_runtime::{sleep, JoinHandle}; use parking_lot::Mutex; From 16d0294abfc99ff44fcb8a0b2cb43958df122596 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:43:17 +1100 Subject: [PATCH 229/232] Upgrade protocol to v1.44.0 --- livekit-protocol/protocol | 2 +- livekit-protocol/src/livekit.rs | 303 +- livekit-protocol/src/livekit.serde.rs | 4985 ++++++++++++++++++------- 3 files changed, 3854 insertions(+), 1436 deletions(-) diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index fcc48786b..765a80e42 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit fcc48786b42607b8ba87e840a5c1d39e00b5f4e9 +Subproject commit 765a80e4298e376593859c3f11cf748c725f68f9 diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index da058ae5a..fe99a4674 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -88,6 +88,8 @@ pub struct MetricsRecordingHeader { pub duration: u64, #[prost(message, optional, tag="4")] pub start_time: ::core::option::Option<::pbjson_types::Timestamp>, + #[prost(map="string, string", tag="5")] + pub room_tags: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>, } // // Protocol used to record metrics for a specific session. @@ -357,6 +359,8 @@ pub struct ParticipantInfo { pub disconnect_reason: i32, #[prost(enumeration="participant_info::KindDetail", repeated, tag="18")] pub kind_details: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="19")] + pub data_tracks: ::prost::alloc::vec::Vec, } /// Nested message and enum types in `ParticipantInfo`. pub mod participant_info { @@ -410,9 +414,11 @@ pub mod participant_info { /// LiveKit agents Agent = 4, /// Connectors participants - /// - /// NEXT_ID: 8 Connector = 7, + /// Bridge participants + /// + /// NEXT_ID: 9 + Bridge = 8, } impl Kind { /// String value of the enum field names used in the ProtoBuf definition. @@ -427,6 +433,7 @@ pub mod participant_info { Kind::Sip => "SIP", Kind::Agent => "AGENT", Kind::Connector => "CONNECTOR", + Kind::Bridge => "BRIDGE", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -438,6 +445,7 @@ pub mod participant_info { "SIP" => Some(Self::Sip), "AGENT" => Some(Self::Agent), "CONNECTOR" => Some(Self::Connector), + "BRIDGE" => Some(Self::Bridge), _ => None, } } @@ -449,6 +457,8 @@ pub mod participant_info { Forwarded = 1, ConnectorWhatsapp = 2, ConnectorTwilio = 3, + /// NEXT_ID: 5 + BridgeRtsp = 4, } impl KindDetail { /// String value of the enum field names used in the ProtoBuf definition. @@ -461,6 +471,7 @@ pub mod participant_info { KindDetail::Forwarded => "FORWARDED", KindDetail::ConnectorWhatsapp => "CONNECTOR_WHATSAPP", KindDetail::ConnectorTwilio => "CONNECTOR_TWILIO", + KindDetail::BridgeRtsp => "BRIDGE_RTSP", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -470,6 +481,7 @@ pub mod participant_info { "FORWARDED" => Some(Self::Forwarded), "CONNECTOR_WHATSAPP" => Some(Self::ConnectorWhatsapp), "CONNECTOR_TWILIO" => Some(Self::ConnectorTwilio), + "BRIDGE_RTSP" => Some(Self::BridgeRtsp), _ => None, } } @@ -593,6 +605,38 @@ pub struct TrackInfo { #[prost(enumeration="BackupCodecPolicy", tag="20")] pub backup_codec_policy: i32, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackInfo { + /// Client-assigned, 16-bit identifier that will be attached to packets sent by the publisher. + #[prost(uint32, tag="1")] + pub pub_handle: u32, + /// Server-assigned track identifier. + #[prost(string, tag="2")] + pub sid: ::prost::alloc::string::String, + /// Human-readable identifier (e.g., `geoLocation`, `servoPosition.x`, etc.), unique per publisher. + #[prost(string, tag="3")] + pub name: ::prost::alloc::string::String, + /// Method used for end-to-end encryption (E2EE) on packet payloads. + #[prost(enumeration="encryption::Type", tag="4")] + pub encryption: i32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackExtensionParticipantSid { + #[prost(enumeration="DataTrackExtensionId", tag="1")] + pub id: i32, + #[prost(string, tag="2")] + pub participant_sid: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackSubscriptionOptions { + /// Rate in frames per second (FPS) the subscriber wants to receive frames at. + /// If omitted, the subscriber defaults to the publisher's fps + #[prost(uint32, optional, tag="1")] + pub target_fps: ::core::option::Option, +} /// provide information about available spatial layers #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -613,6 +657,8 @@ pub struct VideoLayer { pub spatial_layer: i32, #[prost(string, tag="7")] pub rid: ::prost::alloc::string::String, + #[prost(uint32, tag="8")] + pub repair_ssrc: u32, } /// Nested message and enum types in `VideoLayer`. pub mod video_layer { @@ -1681,6 +1727,32 @@ impl TrackSource { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] +pub enum DataTrackExtensionId { + DteiInvalid = 0, + DteiParticipantSid = 1, +} +impl DataTrackExtensionId { + /// 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 { + DataTrackExtensionId::DteiInvalid => "DTEI_INVALID", + DataTrackExtensionId::DteiParticipantSid => "DTEI_PARTICIPANT_SID", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "DTEI_INVALID" => Some(Self::DteiInvalid), + "DTEI_PARTICIPANT_SID" => Some(Self::DteiParticipantSid), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] pub enum VideoQuality { Low = 0, Medium = 1, @@ -3044,7 +3116,7 @@ impl EgressSourceType { #[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")] + #[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")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalRequest`. @@ -3103,12 +3175,21 @@ pub mod signal_request { /// Update local video track settings #[prost(message, tag="18")] UpdateVideoTrack(super::UpdateLocalVideoTrack), + /// Publish a data track + #[prost(message, tag="19")] + PublishDataTrackRequest(super::PublishDataTrackRequest), + /// Unpublish a data track + #[prost(message, tag="20")] + UnpublishDataTrackRequest(super::UnpublishDataTrackRequest), + /// Update subscription state for one or more data tracks + #[prost(message, tag="21")] + UpdateDataSubscription(super::UpdateDataSubscription), } } #[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")] + #[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")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalResponse`. @@ -3194,6 +3275,15 @@ pub mod signal_response { /// when audio subscription changes, used to enable simulcasting of audio codecs based on subscriptions #[prost(message, tag="26")] SubscribedAudioCodecUpdate(super::SubscribedAudioCodecUpdate), + /// Sent in response to `PublishDataTrackRequest`. + #[prost(message, tag="27")] + PublishDataTrackResponse(super::PublishDataTrackResponse), + /// Sent in response to `UnpublishDataTrackRequest` or SFU-initiated unpublish. + #[prost(message, tag="28")] + UnpublishDataTrackResponse(super::UnpublishDataTrackResponse), + /// Sent to data track subscribers to provide mapping from track SIDs to handles. + #[prost(message, tag="29")] + DataTrackSubscriberHandles(super::DataTrackSubscriberHandles), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -3260,6 +3350,62 @@ pub struct AddTrackRequest { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct PublishDataTrackRequest { + /// Client-assigned, 16-bit identifier that will be attached to packets sent by the publisher. + /// This must be non-zero and unique for each data track published by the publisher. + #[prost(uint32, tag="1")] + pub pub_handle: u32, + /// Human-readable identifier (e.g., `geoLocation`, `servoPosition.x`, etc.), unique per publisher. + /// This must be non-empty and no longer than 256 characters. + #[prost(string, tag="2")] + pub name: ::prost::alloc::string::String, + /// Method used for end-to-end encryption (E2EE) on frame payloads. + #[prost(enumeration="encryption::Type", tag="3")] + pub encryption: i32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PublishDataTrackResponse { + /// Information about the published track. + #[prost(message, optional, tag="1")] + pub info: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UnpublishDataTrackRequest { + /// Publisher handle of the track to unpublish. + #[prost(uint32, tag="1")] + pub pub_handle: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UnpublishDataTrackResponse { + /// Information about the unpublished track. + #[prost(message, optional, tag="1")] + pub info: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackSubscriberHandles { + /// Maps handles from incoming packets to the track SIDs that the packets belong to. + #[prost(map="uint32, message", tag="1")] + pub sub_handles: ::std::collections::HashMap, +} +/// Nested message and enum types in `DataTrackSubscriberHandles`. +pub mod data_track_subscriber_handles { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] + pub struct PublishedDataTrack { + #[prost(string, tag="1")] + pub publisher_identity: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub publisher_sid: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub track_sid: ::prost::alloc::string::String, + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct TrickleRequest { #[prost(string, tag="1")] pub candidate_init: ::prost::alloc::string::String, @@ -3375,6 +3521,27 @@ pub struct UpdateSubscription { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct UpdateDataSubscription { + #[prost(message, repeated, tag="1")] + pub updates: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `UpdateDataSubscription`. +pub mod update_data_subscription { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] + pub struct Update { + #[prost(string, tag="1")] + pub track_sid: ::prost::alloc::string::String, + #[prost(bool, tag="2")] + pub subscribe: bool, + /// Options to apply when initially subscribing or updating an existing subscription. + /// When unsubscribing, this field is ignored. + #[prost(message, optional, tag="3")] + pub options: ::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>, @@ -3651,6 +3818,8 @@ pub struct SyncState { pub track_sids_disabled: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(message, repeated, tag="7")] pub datachannel_receive_states: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="8")] + pub publish_data_tracks: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3762,7 +3931,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")] + #[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`. @@ -3777,6 +3946,10 @@ pub mod request_response { Queued = 4, UnsupportedType = 5, UnclassifiedError = 6, + InvalidHandle = 7, + InvalidName = 8, + DuplicateHandle = 9, + DuplicateName = 10, } impl Reason { /// String value of the enum field names used in the ProtoBuf definition. @@ -3792,6 +3965,10 @@ pub mod request_response { Reason::Queued => "QUEUED", Reason::UnsupportedType => "UNSUPPORTED_TYPE", Reason::UnclassifiedError => "UNCLASSIFIED_ERROR", + Reason::InvalidHandle => "INVALID_HANDLE", + Reason::InvalidName => "INVALID_NAME", + Reason::DuplicateHandle => "DUPLICATE_HANDLE", + Reason::DuplicateName => "DUPLICATE_NAME", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -3804,6 +3981,10 @@ pub mod request_response { "QUEUED" => Some(Self::Queued), "UNSUPPORTED_TYPE" => Some(Self::UnsupportedType), "UNCLASSIFIED_ERROR" => Some(Self::UnclassifiedError), + "INVALID_HANDLE" => Some(Self::InvalidHandle), + "INVALID_NAME" => Some(Self::InvalidName), + "DUPLICATE_HANDLE" => Some(Self::DuplicateHandle), + "DUPLICATE_NAME" => Some(Self::DuplicateName), _ => None, } } @@ -3823,6 +4004,10 @@ pub mod request_response { UpdateAudioTrack(super::UpdateLocalAudioTrack), #[prost(message, tag="9")] UpdateVideoTrack(super::UpdateLocalVideoTrack), + #[prost(message, tag="10")] + PublishDataTrack(super::PublishDataTrackRequest), + #[prost(message, tag="11")] + UnpublishDataTrack(super::UnpublishDataTrackRequest), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -3842,6 +4027,8 @@ pub struct ConnectionSettings { pub subscriber_allow_pause: ::core::option::Option, #[prost(bool, tag="4")] pub disable_ice_lite: bool, + #[prost(bool, optional, tag="5")] + pub auto_subscribe_data_track: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -4047,7 +4234,7 @@ pub struct JobState { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct WorkerMessage { - #[prost(oneof="worker_message::Message", tags="1, 2, 3, 4, 5, 6, 7")] + #[prost(oneof="worker_message::Message", tags="1, 2, 3, 4, 5, 6, 7, 8, 9")] pub message: ::core::option::Option, } /// Nested message and enum types in `WorkerMessage`. @@ -4073,13 +4260,17 @@ pub mod worker_message { SimulateJob(super::SimulateJobRequest), #[prost(message, tag="7")] MigrateJob(super::MigrateJobRequest), + #[prost(message, tag="8")] + TextResponse(super::TextMessageResponse), + #[prost(message, tag="9")] + PushText(super::PushTextRequest), } } /// from Server to Worker #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ServerMessage { - #[prost(oneof="server_message::Message", tags="1, 2, 3, 5, 4")] + #[prost(oneof="server_message::Message", tags="1, 2, 3, 5, 4, 6")] pub message: ::core::option::Option, } /// Nested message and enum types in `ServerMessage`. @@ -4099,6 +4290,8 @@ pub mod server_message { Termination(super::JobTermination), #[prost(message, tag="4")] Pong(super::WorkerPong), + #[prost(message, tag="6")] + TextRequest(super::TextMessageRequest), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -4228,6 +4421,42 @@ pub struct JobTermination { #[prost(string, tag="1")] pub job_id: ::prost::alloc::string::String, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TextMessageRequest { + #[prost(string, tag="1")] + pub message_id: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub session_id: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub agent_name: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub metadata: ::prost::alloc::string::String, + #[prost(bytes="vec", tag="5")] + pub session_data: ::prost::alloc::vec::Vec, + #[prost(string, tag="6")] + pub text: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PushTextRequest { + /// The message_id of the TextMessageRequest that this push is for + #[prost(string, tag="1")] + pub message_id: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub content: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TextMessageResponse { + /// Indicate the request is completed + #[prost(string, tag="1")] + pub message_id: ::prost::alloc::string::String, + #[prost(bytes="vec", tag="2")] + pub session_data: ::prost::alloc::vec::Vec, + #[prost(string, tag="3")] + pub error: ::prost::alloc::string::String, +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum JobType { @@ -4374,7 +4603,7 @@ pub struct AgentDispatch { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AgentDispatchState { - /// For dispatches of tyoe JT_ROOM, there will be at most 1 job. + /// For dispatches of tyoe JT_ROOM, there will be at most 1 job. /// For dispatches of type JT_PUBLISHER, there will be 1 per publisher. #[prost(message, repeated, tag="1")] pub jobs: ::prost::alloc::vec::Vec, @@ -5678,9 +5907,12 @@ pub struct SipDispatchRuleInfo { pub trunk_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(bool, tag="4")] pub hide_phone_number: bool, - /// Dispatch Rule will only accept a call made to these numbers (if set). + /// Dispatch Rule will only accept a call made from these numbers (if set). #[prost(string, repeated, tag="7")] pub inbound_numbers: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + /// Dispatch Rule will only accept a call made to these numbers (if set). + #[prost(string, repeated, tag="13")] + pub numbers: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, /// Human-readable name for the Dispatch Rule. #[prost(string, tag="5")] pub name: ::prost::alloc::string::String, @@ -5700,7 +5932,7 @@ pub struct SipDispatchRuleInfo { pub room_config: ::core::option::Option, #[prost(bool, tag="11")] pub krisp_enabled: bool, - /// NEXT ID: 13 + /// NEXT ID: 14 #[prost(enumeration="SipMediaEncryption", tag="12")] pub media_encryption: i32, } @@ -5947,6 +6179,8 @@ pub struct SipCallInfo { pub call_context: ::prost::alloc::vec::Vec<::pbjson_types::Any>, #[prost(message, optional, tag="27")] pub provider_info: ::core::option::Option, + #[prost(string, tag="28")] + pub sip_call_id: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -6494,7 +6728,7 @@ impl SipCallDirection { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DialWhatsAppCallRequest { - /// Required - The number of the business that is initiating the call + /// Required - The phone number id of the business that is initiating the call #[prost(string, tag="1")] pub whatsapp_phone_number_id: ::prost::alloc::string::String, /// Required - The number of the user that is supossed to receive the call @@ -6516,6 +6750,7 @@ pub struct DialWhatsAppCallRequest { #[prost(message, repeated, tag="6")] pub agents: ::prost::alloc::vec::Vec, /// Optional - Identity of the participant in LiveKit room + /// This is used for logging purposes, so it is advised to not put PII in this field. #[prost(string, tag="7")] pub participant_identity: ::prost::alloc::string::String, /// Optional - Name of the participant in LiveKit room @@ -6549,9 +6784,49 @@ pub struct DisconnectWhatsAppCallRequest { /// Required - Call ID sent by Meta #[prost(string, tag="1")] pub whatsapp_call_id: ::prost::alloc::string::String, - /// Required - The API key of the business that is disconnecting the call + /// The API key of the whatsapp business. + /// Required if the DisconnectReason is BUSINESS_INITIATED. + /// Optional for USER_INITIATED as no API call to WhatsApp is needed. #[prost(string, tag="2")] pub whatsapp_api_key: ::prost::alloc::string::String, + /// The reason for disconnecting the call + #[prost(enumeration="disconnect_whats_app_call_request::DisconnectReason", tag="3")] + pub disconnect_reason: i32, +} +/// Nested message and enum types in `DisconnectWhatsAppCallRequest`. +pub mod disconnect_whats_app_call_request { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum DisconnectReason { + /// The call is being disconnected by the business + BusinessInitiated = 0, + /// The call is disconnected by the user. + /// This can be tracked as part of call terminate webhook + /// + /// Note that this webhook will also be sent when the call is disconnected by the business. + /// Calling the API twice in such cases will result in an error. + UserInitiated = 1, + } + impl DisconnectReason { + /// 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 { + DisconnectReason::BusinessInitiated => "BUSINESS_INITIATED", + DisconnectReason::UserInitiated => "USER_INITIATED", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "BUSINESS_INITIATED" => Some(Self::BusinessInitiated), + "USER_INITIATED" => Some(Self::UserInitiated), + _ => None, + } + } + } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -6575,7 +6850,7 @@ pub struct ConnectWhatsAppCallResponse { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AcceptWhatsAppCallRequest { - /// Required - The number of the business that is conencting the call + /// Required - The phone number id of the business that is conencting the call #[prost(string, tag="1")] pub whatsapp_phone_number_id: ::prost::alloc::string::String, /// Required - The API key of the business that is connecting the call @@ -6601,6 +6876,7 @@ pub struct AcceptWhatsAppCallRequest { #[prost(message, repeated, tag="7")] pub agents: ::prost::alloc::vec::Vec, /// Optional - Identity of the participant in LiveKit room + /// This is used for logging purposes, so it is advised to not put PII in this field. #[prost(string, tag="8")] pub participant_identity: ::prost::alloc::string::String, /// Optional - Name of the participant in LiveKit room @@ -6674,6 +6950,7 @@ pub struct ConnectTwilioCallRequest { #[prost(message, repeated, tag="3")] pub agents: ::prost::alloc::vec::Vec, /// Optional identity of the participant in LiveKit room + /// This is used for logging purposes, so it is advised to not put PII in this field. #[prost(string, tag="4")] pub participant_identity: ::prost::alloc::string::String, /// Optional name of the participant in LiveKit room diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index ae66f2b6c..7739e401e 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -4364,6 +4364,9 @@ impl serde::Serialize for ConnectionSettings { if self.disable_ice_lite { len += 1; } + if self.auto_subscribe_data_track.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.ConnectionSettings", len)?; if self.auto_subscribe { struct_ser.serialize_field("autoSubscribe", &self.auto_subscribe)?; @@ -4377,6 +4380,9 @@ impl serde::Serialize for ConnectionSettings { if self.disable_ice_lite { struct_ser.serialize_field("disableIceLite", &self.disable_ice_lite)?; } + if let Some(v) = self.auto_subscribe_data_track.as_ref() { + struct_ser.serialize_field("autoSubscribeDataTrack", v)?; + } struct_ser.end() } } @@ -4395,6 +4401,8 @@ impl<'de> serde::Deserialize<'de> for ConnectionSettings { "subscriberAllowPause", "disable_ice_lite", "disableIceLite", + "auto_subscribe_data_track", + "autoSubscribeDataTrack", ]; #[allow(clippy::enum_variant_names)] @@ -4403,6 +4411,7 @@ impl<'de> serde::Deserialize<'de> for ConnectionSettings { AdaptiveStream, SubscriberAllowPause, DisableIceLite, + AutoSubscribeDataTrack, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -4429,6 +4438,7 @@ impl<'de> serde::Deserialize<'de> for ConnectionSettings { "adaptiveStream" | "adaptive_stream" => Ok(GeneratedField::AdaptiveStream), "subscriberAllowPause" | "subscriber_allow_pause" => Ok(GeneratedField::SubscriberAllowPause), "disableIceLite" | "disable_ice_lite" => Ok(GeneratedField::DisableIceLite), + "autoSubscribeDataTrack" | "auto_subscribe_data_track" => Ok(GeneratedField::AutoSubscribeDataTrack), _ => Ok(GeneratedField::__SkipField__), } } @@ -4452,6 +4462,7 @@ impl<'de> serde::Deserialize<'de> for ConnectionSettings { let mut adaptive_stream__ = None; let mut subscriber_allow_pause__ = None; let mut disable_ice_lite__ = None; + let mut auto_subscribe_data_track__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::AutoSubscribe => { @@ -4478,6 +4489,12 @@ impl<'de> serde::Deserialize<'de> for ConnectionSettings { } disable_ice_lite__ = Some(map_.next_value()?); } + GeneratedField::AutoSubscribeDataTrack => { + if auto_subscribe_data_track__.is_some() { + return Err(serde::de::Error::duplicate_field("autoSubscribeDataTrack")); + } + auto_subscribe_data_track__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -4488,6 +4505,7 @@ impl<'de> serde::Deserialize<'de> for ConnectionSettings { adaptive_stream: adaptive_stream__.unwrap_or_default(), subscriber_allow_pause: subscriber_allow_pause__, disable_ice_lite: disable_ice_lite__.unwrap_or_default(), + auto_subscribe_data_track: auto_subscribe_data_track__, }) } } @@ -8157,120 +8175,78 @@ impl<'de> serde::Deserialize<'de> for data_stream::Trailer { deserializer.deserialize_struct("livekit.DataStream.Trailer", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DeleteAgentDispatchRequest { +impl serde::Serialize for DataTrackExtensionId { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { - use serde::ser::SerializeStruct; - let mut len = 0; - if !self.dispatch_id.is_empty() { - len += 1; - } - if !self.room.is_empty() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.DeleteAgentDispatchRequest", len)?; - if !self.dispatch_id.is_empty() { - struct_ser.serialize_field("dispatchId", &self.dispatch_id)?; - } - if !self.room.is_empty() { - struct_ser.serialize_field("room", &self.room)?; - } - struct_ser.end() + let variant = match self { + Self::DteiInvalid => "DTEI_INVALID", + Self::DteiParticipantSid => "DTEI_PARTICIPANT_SID", + }; + serializer.serialize_str(variant) } } -impl<'de> serde::Deserialize<'de> for DeleteAgentDispatchRequest { +impl<'de> serde::Deserialize<'de> for DataTrackExtensionId { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "dispatch_id", - "dispatchId", - "room", + "DTEI_INVALID", + "DTEI_PARTICIPANT_SID", ]; - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - DispatchId, - Room, - __SkipField__, - } - impl<'de> serde::Deserialize<'de> for GeneratedField { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - struct GeneratedVisitor; + struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = GeneratedField; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackExtensionId; - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "expected one of: {:?}", &FIELDS) - } + 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 { - "dispatchId" | "dispatch_id" => Ok(GeneratedField::DispatchId), - "room" => Ok(GeneratedField::Room), - _ => Ok(GeneratedField::__SkipField__), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) + 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) + }) } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DeleteAgentDispatchRequest; - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DeleteAgentDispatchRequest") + 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_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, { - let mut dispatch_id__ = None; - let mut room__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::DispatchId => { - if dispatch_id__.is_some() { - return Err(serde::de::Error::duplicate_field("dispatchId")); - } - dispatch_id__ = Some(map_.next_value()?); - } - GeneratedField::Room => { - if room__.is_some() { - return Err(serde::de::Error::duplicate_field("room")); - } - room__ = Some(map_.next_value()?); - } - GeneratedField::__SkipField__ => { - let _ = map_.next_value::()?; - } - } + match value { + "DTEI_INVALID" => Ok(DataTrackExtensionId::DteiInvalid), + "DTEI_PARTICIPANT_SID" => Ok(DataTrackExtensionId::DteiParticipantSid), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } - Ok(DeleteAgentDispatchRequest { - dispatch_id: dispatch_id__.unwrap_or_default(), - room: room__.unwrap_or_default(), - }) } } - deserializer.deserialize_struct("livekit.DeleteAgentDispatchRequest", FIELDS, GeneratedVisitor) + deserializer.deserialize_any(GeneratedVisitor) } } -impl serde::Serialize for DeleteIngressRequest { +impl serde::Serialize for DataTrackExtensionParticipantSid { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -8278,30 +8254,40 @@ impl serde::Serialize for DeleteIngressRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.ingress_id.is_empty() { + if self.id != 0 { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.DeleteIngressRequest", len)?; - if !self.ingress_id.is_empty() { - struct_ser.serialize_field("ingressId", &self.ingress_id)?; + if !self.participant_sid.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackExtensionParticipantSid", len)?; + if self.id != 0 { + let v = DataTrackExtensionId::try_from(self.id) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.id)))?; + struct_ser.serialize_field("id", &v)?; + } + if !self.participant_sid.is_empty() { + struct_ser.serialize_field("participantSid", &self.participant_sid)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DeleteIngressRequest { +impl<'de> serde::Deserialize<'de> for DataTrackExtensionParticipantSid { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "ingress_id", - "ingressId", + "id", + "participant_sid", + "participantSid", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - IngressId, + Id, + ParticipantSid, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -8324,7 +8310,8 @@ impl<'de> serde::Deserialize<'de> for DeleteIngressRequest { E: serde::de::Error, { match value { - "ingressId" | "ingress_id" => Ok(GeneratedField::IngressId), + "id" => Ok(GeneratedField::Id), + "participantSid" | "participant_sid" => Ok(GeneratedField::ParticipantSid), _ => Ok(GeneratedField::__SkipField__), } } @@ -8334,39 +8321,47 @@ impl<'de> serde::Deserialize<'de> for DeleteIngressRequest { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DeleteIngressRequest; + type Value = DataTrackExtensionParticipantSid; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DeleteIngressRequest") + formatter.write_str("struct livekit.DataTrackExtensionParticipantSid") } - 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 ingress_id__ = None; + let mut id__ = None; + let mut participant_sid__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::IngressId => { - if ingress_id__.is_some() { - return Err(serde::de::Error::duplicate_field("ingressId")); + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); } - ingress_id__ = Some(map_.next_value()?); + id__ = Some(map_.next_value::()? as i32); + } + GeneratedField::ParticipantSid => { + if participant_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("participantSid")); + } + participant_sid__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(DeleteIngressRequest { - ingress_id: ingress_id__.unwrap_or_default(), + Ok(DataTrackExtensionParticipantSid { + id: id__.unwrap_or_default(), + participant_sid: participant_sid__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.DeleteIngressRequest", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DataTrackExtensionParticipantSid", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DeleteRoomRequest { +impl serde::Serialize for DataTrackInfo { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -8374,116 +8369,56 @@ impl serde::Serialize for DeleteRoomRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.room.is_empty() { + if self.pub_handle != 0 { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.DeleteRoomRequest", len)?; - if !self.room.is_empty() { - struct_ser.serialize_field("room", &self.room)?; + if !self.sid.is_empty() { + len += 1; } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for DeleteRoomRequest { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "room", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - Room, - __SkipField__, + if !self.name.is_empty() { + len += 1; } - 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 { - "room" => Ok(GeneratedField::Room), - _ => Ok(GeneratedField::__SkipField__), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } + if self.encryption != 0 { + len += 1; } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DeleteRoomRequest; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DeleteRoomRequest") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut 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::__SkipField__ => { - let _ = map_.next_value::()?; - } - } - } - Ok(DeleteRoomRequest { - room: room__.unwrap_or_default(), - }) - } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackInfo", len)?; + if self.pub_handle != 0 { + struct_ser.serialize_field("pubHandle", &self.pub_handle)?; + } + if !self.sid.is_empty() { + struct_ser.serialize_field("sid", &self.sid)?; + } + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + if self.encryption != 0 { + let v = encryption::Type::try_from(self.encryption) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; + struct_ser.serialize_field("encryption", &v)?; } - deserializer.deserialize_struct("livekit.DeleteRoomRequest", FIELDS, GeneratedVisitor) - } -} -impl serde::Serialize for DeleteRoomResponse { - #[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.DeleteRoomResponse", len)?; struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DeleteRoomResponse { +impl<'de> serde::Deserialize<'de> for DataTrackInfo { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ + "pub_handle", + "pubHandle", + "sid", + "name", + "encryption", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { + PubHandle, + Sid, + Name, + Encryption, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -8505,7 +8440,13 @@ impl<'de> serde::Deserialize<'de> for DeleteRoomResponse { where E: serde::de::Error, { - Ok(GeneratedField::__SkipField__) + match value { + "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), + "sid" => Ok(GeneratedField::Sid), + "name" => Ok(GeneratedField::Name), + "encryption" => Ok(GeneratedField::Encryption), + _ => Ok(GeneratedField::__SkipField__), + } } } deserializer.deserialize_identifier(GeneratedVisitor) @@ -8513,27 +8454,65 @@ impl<'de> serde::Deserialize<'de> for DeleteRoomResponse { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DeleteRoomResponse; + type Value = DataTrackInfo; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DeleteRoomResponse") + formatter.write_str("struct livekit.DataTrackInfo") } - 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 pub_handle__ = None; + let mut sid__ = None; + let mut name__ = None; + let mut encryption__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::PubHandle => { + if pub_handle__.is_some() { + return Err(serde::de::Error::duplicate_field("pubHandle")); + } + pub_handle__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Sid => { + if sid__.is_some() { + return Err(serde::de::Error::duplicate_field("sid")); + } + sid__ = Some(map_.next_value()?); + } + GeneratedField::Name => { + if name__.is_some() { + return Err(serde::de::Error::duplicate_field("name")); + } + name__ = Some(map_.next_value()?); + } + GeneratedField::Encryption => { + if encryption__.is_some() { + return Err(serde::de::Error::duplicate_field("encryption")); + } + encryption__ = Some(map_.next_value::()? as i32); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } } - Ok(DeleteRoomResponse { + Ok(DataTrackInfo { + pub_handle: pub_handle__.unwrap_or_default(), + sid: sid__.unwrap_or_default(), + name: name__.unwrap_or_default(), + encryption: encryption__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.DeleteRoomResponse", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DataTrackInfo", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DeleteSipDispatchRuleRequest { +impl serde::Serialize for DataTrackSubscriberHandles { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -8541,30 +8520,30 @@ impl serde::Serialize for DeleteSipDispatchRuleRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.sip_dispatch_rule_id.is_empty() { + if !self.sub_handles.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.DeleteSIPDispatchRuleRequest", len)?; - if !self.sip_dispatch_rule_id.is_empty() { - struct_ser.serialize_field("sipDispatchRuleId", &self.sip_dispatch_rule_id)?; + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSubscriberHandles", len)?; + if !self.sub_handles.is_empty() { + struct_ser.serialize_field("subHandles", &self.sub_handles)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DeleteSipDispatchRuleRequest { +impl<'de> serde::Deserialize<'de> for DataTrackSubscriberHandles { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "sip_dispatch_rule_id", - "sipDispatchRuleId", + "sub_handles", + "subHandles", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - SipDispatchRuleId, + SubHandles, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -8587,7 +8566,7 @@ impl<'de> serde::Deserialize<'de> for DeleteSipDispatchRuleRequest { E: serde::de::Error, { match value { - "sipDispatchRuleId" | "sip_dispatch_rule_id" => Ok(GeneratedField::SipDispatchRuleId), + "subHandles" | "sub_handles" => Ok(GeneratedField::SubHandles), _ => Ok(GeneratedField::__SkipField__), } } @@ -8597,39 +8576,42 @@ impl<'de> serde::Deserialize<'de> for DeleteSipDispatchRuleRequest { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DeleteSipDispatchRuleRequest; + type Value = DataTrackSubscriberHandles; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DeleteSIPDispatchRuleRequest") + formatter.write_str("struct livekit.DataTrackSubscriberHandles") } - 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 sip_dispatch_rule_id__ = None; + let mut sub_handles__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::SipDispatchRuleId => { - if sip_dispatch_rule_id__.is_some() { - return Err(serde::de::Error::duplicate_field("sipDispatchRuleId")); + GeneratedField::SubHandles => { + if sub_handles__.is_some() { + return Err(serde::de::Error::duplicate_field("subHandles")); } - sip_dispatch_rule_id__ = Some(map_.next_value()?); + sub_handles__ = Some( + map_.next_value::, _>>()? + .into_iter().map(|(k,v)| (k.0, v)).collect() + ); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(DeleteSipDispatchRuleRequest { - sip_dispatch_rule_id: sip_dispatch_rule_id__.unwrap_or_default(), + Ok(DataTrackSubscriberHandles { + sub_handles: sub_handles__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.DeleteSIPDispatchRuleRequest", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DataTrackSubscriberHandles", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DeleteSipTrunkRequest { +impl serde::Serialize for data_track_subscriber_handles::PublishedDataTrack { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -8637,30 +8619,48 @@ impl serde::Serialize for DeleteSipTrunkRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.sip_trunk_id.is_empty() { + if !self.publisher_identity.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.DeleteSIPTrunkRequest", len)?; - if !self.sip_trunk_id.is_empty() { - struct_ser.serialize_field("sipTrunkId", &self.sip_trunk_id)?; + if !self.publisher_sid.is_empty() { + len += 1; + } + if !self.track_sid.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSubscriberHandles.PublishedDataTrack", len)?; + if !self.publisher_identity.is_empty() { + struct_ser.serialize_field("publisherIdentity", &self.publisher_identity)?; + } + if !self.publisher_sid.is_empty() { + struct_ser.serialize_field("publisherSid", &self.publisher_sid)?; + } + if !self.track_sid.is_empty() { + struct_ser.serialize_field("trackSid", &self.track_sid)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DeleteSipTrunkRequest { +impl<'de> serde::Deserialize<'de> for data_track_subscriber_handles::PublishedDataTrack { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "sip_trunk_id", - "sipTrunkId", + "publisher_identity", + "publisherIdentity", + "publisher_sid", + "publisherSid", + "track_sid", + "trackSid", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - SipTrunkId, + PublisherIdentity, + PublisherSid, + TrackSid, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -8683,7 +8683,9 @@ impl<'de> serde::Deserialize<'de> for DeleteSipTrunkRequest { E: serde::de::Error, { match value { - "sipTrunkId" | "sip_trunk_id" => Ok(GeneratedField::SipTrunkId), + "publisherIdentity" | "publisher_identity" => Ok(GeneratedField::PublisherIdentity), + "publisherSid" | "publisher_sid" => Ok(GeneratedField::PublisherSid), + "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), _ => Ok(GeneratedField::__SkipField__), } } @@ -8693,39 +8695,55 @@ impl<'de> serde::Deserialize<'de> for DeleteSipTrunkRequest { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DeleteSipTrunkRequest; + type Value = data_track_subscriber_handles::PublishedDataTrack; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DeleteSIPTrunkRequest") + formatter.write_str("struct livekit.DataTrackSubscriberHandles.PublishedDataTrack") } - 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 sip_trunk_id__ = None; + let mut publisher_identity__ = None; + let mut publisher_sid__ = None; + let mut track_sid__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::SipTrunkId => { - if sip_trunk_id__.is_some() { - return Err(serde::de::Error::duplicate_field("sipTrunkId")); + GeneratedField::PublisherIdentity => { + if publisher_identity__.is_some() { + return Err(serde::de::Error::duplicate_field("publisherIdentity")); } - sip_trunk_id__ = Some(map_.next_value()?); + publisher_identity__ = Some(map_.next_value()?); + } + GeneratedField::PublisherSid => { + if publisher_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("publisherSid")); + } + publisher_sid__ = Some(map_.next_value()?); + } + GeneratedField::TrackSid => { + if track_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSid")); + } + track_sid__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(DeleteSipTrunkRequest { - sip_trunk_id: sip_trunk_id__.unwrap_or_default(), + Ok(data_track_subscriber_handles::PublishedDataTrack { + publisher_identity: publisher_identity__.unwrap_or_default(), + publisher_sid: publisher_sid__.unwrap_or_default(), + track_sid: track_sid__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.DeleteSIPTrunkRequest", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DataTrackSubscriberHandles.PublishedDataTrack", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for Destination { +impl serde::Serialize for DataTrackSubscriptionOptions { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -8733,45 +8751,30 @@ impl serde::Serialize for Destination { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.city.is_empty() { - len += 1; - } - if !self.country.is_empty() { + if self.target_fps.is_some() { len += 1; } - if !self.region.is_empty() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.Destination", len)?; - if !self.city.is_empty() { - struct_ser.serialize_field("city", &self.city)?; - } - if !self.country.is_empty() { - struct_ser.serialize_field("country", &self.country)?; - } - if !self.region.is_empty() { - struct_ser.serialize_field("region", &self.region)?; + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSubscriptionOptions", len)?; + if let Some(v) = self.target_fps.as_ref() { + struct_ser.serialize_field("targetFps", v)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for Destination { +impl<'de> serde::Deserialize<'de> for DataTrackSubscriptionOptions { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "city", - "country", - "region", + "target_fps", + "targetFps", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - City, - Country, - Region, + TargetFps, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -8794,9 +8797,7 @@ impl<'de> serde::Deserialize<'de> for Destination { E: serde::de::Error, { match value { - "city" => Ok(GeneratedField::City), - "country" => Ok(GeneratedField::Country), - "region" => Ok(GeneratedField::Region), + "targetFps" | "target_fps" => Ok(GeneratedField::TargetFps), _ => Ok(GeneratedField::__SkipField__), } } @@ -8806,55 +8807,41 @@ impl<'de> serde::Deserialize<'de> for Destination { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = Destination; + type Value = DataTrackSubscriptionOptions; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.Destination") + formatter.write_str("struct livekit.DataTrackSubscriptionOptions") } - 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 city__ = None; - let mut country__ = None; - let mut region__ = None; + let mut target_fps__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::City => { - if city__.is_some() { - return Err(serde::de::Error::duplicate_field("city")); - } - city__ = Some(map_.next_value()?); - } - GeneratedField::Country => { - if country__.is_some() { - return Err(serde::de::Error::duplicate_field("country")); - } - country__ = Some(map_.next_value()?); - } - GeneratedField::Region => { - if region__.is_some() { - return Err(serde::de::Error::duplicate_field("region")); + GeneratedField::TargetFps => { + if target_fps__.is_some() { + return Err(serde::de::Error::duplicate_field("targetFps")); } - region__ = Some(map_.next_value()?); + target_fps__ = + map_.next_value::<::std::option::Option<::pbjson::private::NumberDeserialize<_>>>()?.map(|x| x.0) + ; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(Destination { - city: city__.unwrap_or_default(), - country: country__.unwrap_or_default(), - region: region__.unwrap_or_default(), + Ok(DataTrackSubscriptionOptions { + target_fps: target_fps__, }) } } - deserializer.deserialize_struct("livekit.Destination", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DataTrackSubscriptionOptions", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DialWhatsAppCallRequest { +impl serde::Serialize for DeleteAgentDispatchRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -8862,128 +8849,38 @@ impl serde::Serialize for DialWhatsAppCallRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.whatsapp_phone_number_id.is_empty() { - len += 1; - } - if !self.whatsapp_to_phone_number.is_empty() { - len += 1; - } - if !self.whatsapp_api_key.is_empty() { - len += 1; - } - if !self.whatsapp_cloud_api_version.is_empty() { - len += 1; - } - if !self.whatsapp_biz_opaque_callback_data.is_empty() { - len += 1; - } - if !self.room_name.is_empty() { - len += 1; - } - if !self.agents.is_empty() { - len += 1; - } - if !self.participant_identity.is_empty() { - len += 1; - } - if !self.participant_name.is_empty() { - len += 1; - } - if !self.participant_metadata.is_empty() { - len += 1; - } - if !self.participant_attributes.is_empty() { + if !self.dispatch_id.is_empty() { len += 1; } - if !self.destination_country.is_empty() { + if !self.room.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.DialWhatsAppCallRequest", len)?; - if !self.whatsapp_phone_number_id.is_empty() { - struct_ser.serialize_field("whatsappPhoneNumberId", &self.whatsapp_phone_number_id)?; - } - if !self.whatsapp_to_phone_number.is_empty() { - struct_ser.serialize_field("whatsappToPhoneNumber", &self.whatsapp_to_phone_number)?; - } - if !self.whatsapp_api_key.is_empty() { - struct_ser.serialize_field("whatsappApiKey", &self.whatsapp_api_key)?; - } - if !self.whatsapp_cloud_api_version.is_empty() { - struct_ser.serialize_field("whatsappCloudApiVersion", &self.whatsapp_cloud_api_version)?; - } - if !self.whatsapp_biz_opaque_callback_data.is_empty() { - struct_ser.serialize_field("whatsappBizOpaqueCallbackData", &self.whatsapp_biz_opaque_callback_data)?; - } - if !self.room_name.is_empty() { - struct_ser.serialize_field("roomName", &self.room_name)?; - } - if !self.agents.is_empty() { - struct_ser.serialize_field("agents", &self.agents)?; - } - if !self.participant_identity.is_empty() { - struct_ser.serialize_field("participantIdentity", &self.participant_identity)?; - } - if !self.participant_name.is_empty() { - struct_ser.serialize_field("participantName", &self.participant_name)?; - } - if !self.participant_metadata.is_empty() { - struct_ser.serialize_field("participantMetadata", &self.participant_metadata)?; - } - if !self.participant_attributes.is_empty() { - struct_ser.serialize_field("participantAttributes", &self.participant_attributes)?; + let mut struct_ser = serializer.serialize_struct("livekit.DeleteAgentDispatchRequest", len)?; + if !self.dispatch_id.is_empty() { + struct_ser.serialize_field("dispatchId", &self.dispatch_id)?; } - if !self.destination_country.is_empty() { - struct_ser.serialize_field("destinationCountry", &self.destination_country)?; + if !self.room.is_empty() { + struct_ser.serialize_field("room", &self.room)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DialWhatsAppCallRequest { +impl<'de> serde::Deserialize<'de> for DeleteAgentDispatchRequest { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "whatsapp_phone_number_id", - "whatsappPhoneNumberId", - "whatsapp_to_phone_number", - "whatsappToPhoneNumber", - "whatsapp_api_key", - "whatsappApiKey", - "whatsapp_cloud_api_version", - "whatsappCloudApiVersion", - "whatsapp_biz_opaque_callback_data", - "whatsappBizOpaqueCallbackData", - "room_name", - "roomName", - "agents", - "participant_identity", - "participantIdentity", - "participant_name", - "participantName", - "participant_metadata", - "participantMetadata", - "participant_attributes", - "participantAttributes", - "destination_country", - "destinationCountry", + "dispatch_id", + "dispatchId", + "room", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - WhatsappPhoneNumberId, - WhatsappToPhoneNumber, - WhatsappApiKey, - WhatsappCloudApiVersion, - WhatsappBizOpaqueCallbackData, - RoomName, - Agents, - ParticipantIdentity, - ParticipantName, - ParticipantMetadata, - ParticipantAttributes, - DestinationCountry, + DispatchId, + Room, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -9006,18 +8903,8 @@ impl<'de> serde::Deserialize<'de> for DialWhatsAppCallRequest { E: serde::de::Error, { match value { - "whatsappPhoneNumberId" | "whatsapp_phone_number_id" => Ok(GeneratedField::WhatsappPhoneNumberId), - "whatsappToPhoneNumber" | "whatsapp_to_phone_number" => Ok(GeneratedField::WhatsappToPhoneNumber), - "whatsappApiKey" | "whatsapp_api_key" => Ok(GeneratedField::WhatsappApiKey), - "whatsappCloudApiVersion" | "whatsapp_cloud_api_version" => Ok(GeneratedField::WhatsappCloudApiVersion), - "whatsappBizOpaqueCallbackData" | "whatsapp_biz_opaque_callback_data" => Ok(GeneratedField::WhatsappBizOpaqueCallbackData), - "roomName" | "room_name" => Ok(GeneratedField::RoomName), - "agents" => Ok(GeneratedField::Agents), - "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), - "participantName" | "participant_name" => Ok(GeneratedField::ParticipantName), - "participantMetadata" | "participant_metadata" => Ok(GeneratedField::ParticipantMetadata), - "participantAttributes" | "participant_attributes" => Ok(GeneratedField::ParticipantAttributes), - "destinationCountry" | "destination_country" => Ok(GeneratedField::DestinationCountry), + "dispatchId" | "dispatch_id" => Ok(GeneratedField::DispatchId), + "room" => Ok(GeneratedField::Room), _ => Ok(GeneratedField::__SkipField__), } } @@ -9027,129 +8914,47 @@ impl<'de> serde::Deserialize<'de> for DialWhatsAppCallRequest { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DialWhatsAppCallRequest; + type Value = DeleteAgentDispatchRequest; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DialWhatsAppCallRequest") + formatter.write_str("struct livekit.DeleteAgentDispatchRequest") } - 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 whatsapp_phone_number_id__ = None; - let mut whatsapp_to_phone_number__ = None; - let mut whatsapp_api_key__ = None; - let mut whatsapp_cloud_api_version__ = None; - let mut whatsapp_biz_opaque_callback_data__ = None; - let mut room_name__ = None; - let mut agents__ = None; - let mut participant_identity__ = None; - let mut participant_name__ = None; - let mut participant_metadata__ = None; - let mut participant_attributes__ = None; - let mut destination_country__ = None; + let mut dispatch_id__ = None; + let mut room__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::WhatsappPhoneNumberId => { - if whatsapp_phone_number_id__.is_some() { - return Err(serde::de::Error::duplicate_field("whatsappPhoneNumberId")); - } - whatsapp_phone_number_id__ = Some(map_.next_value()?); - } - GeneratedField::WhatsappToPhoneNumber => { - if whatsapp_to_phone_number__.is_some() { - return Err(serde::de::Error::duplicate_field("whatsappToPhoneNumber")); - } - whatsapp_to_phone_number__ = Some(map_.next_value()?); - } - GeneratedField::WhatsappApiKey => { - if whatsapp_api_key__.is_some() { - return Err(serde::de::Error::duplicate_field("whatsappApiKey")); - } - whatsapp_api_key__ = Some(map_.next_value()?); - } - GeneratedField::WhatsappCloudApiVersion => { - if whatsapp_cloud_api_version__.is_some() { - return Err(serde::de::Error::duplicate_field("whatsappCloudApiVersion")); - } - whatsapp_cloud_api_version__ = Some(map_.next_value()?); - } - GeneratedField::WhatsappBizOpaqueCallbackData => { - if whatsapp_biz_opaque_callback_data__.is_some() { - return Err(serde::de::Error::duplicate_field("whatsappBizOpaqueCallbackData")); - } - whatsapp_biz_opaque_callback_data__ = Some(map_.next_value()?); - } - GeneratedField::RoomName => { - if room_name__.is_some() { - return Err(serde::de::Error::duplicate_field("roomName")); - } - room_name__ = Some(map_.next_value()?); - } - GeneratedField::Agents => { - if agents__.is_some() { - return Err(serde::de::Error::duplicate_field("agents")); - } - agents__ = Some(map_.next_value()?); - } - GeneratedField::ParticipantIdentity => { - if participant_identity__.is_some() { - return Err(serde::de::Error::duplicate_field("participantIdentity")); - } - participant_identity__ = Some(map_.next_value()?); - } - GeneratedField::ParticipantName => { - if participant_name__.is_some() { - return Err(serde::de::Error::duplicate_field("participantName")); - } - participant_name__ = Some(map_.next_value()?); - } - GeneratedField::ParticipantMetadata => { - if participant_metadata__.is_some() { - return Err(serde::de::Error::duplicate_field("participantMetadata")); - } - participant_metadata__ = Some(map_.next_value()?); - } - GeneratedField::ParticipantAttributes => { - if participant_attributes__.is_some() { - return Err(serde::de::Error::duplicate_field("participantAttributes")); + GeneratedField::DispatchId => { + if dispatch_id__.is_some() { + return Err(serde::de::Error::duplicate_field("dispatchId")); } - participant_attributes__ = Some( - map_.next_value::>()? - ); + dispatch_id__ = Some(map_.next_value()?); } - GeneratedField::DestinationCountry => { - if destination_country__.is_some() { - return Err(serde::de::Error::duplicate_field("destinationCountry")); + GeneratedField::Room => { + if room__.is_some() { + return Err(serde::de::Error::duplicate_field("room")); } - destination_country__ = Some(map_.next_value()?); + room__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(DialWhatsAppCallRequest { - whatsapp_phone_number_id: whatsapp_phone_number_id__.unwrap_or_default(), - whatsapp_to_phone_number: whatsapp_to_phone_number__.unwrap_or_default(), - whatsapp_api_key: whatsapp_api_key__.unwrap_or_default(), - whatsapp_cloud_api_version: whatsapp_cloud_api_version__.unwrap_or_default(), - whatsapp_biz_opaque_callback_data: whatsapp_biz_opaque_callback_data__.unwrap_or_default(), - room_name: room_name__.unwrap_or_default(), - agents: agents__.unwrap_or_default(), - participant_identity: participant_identity__.unwrap_or_default(), - participant_name: participant_name__.unwrap_or_default(), - participant_metadata: participant_metadata__.unwrap_or_default(), - participant_attributes: participant_attributes__.unwrap_or_default(), - destination_country: destination_country__.unwrap_or_default(), + Ok(DeleteAgentDispatchRequest { + dispatch_id: dispatch_id__.unwrap_or_default(), + room: room__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.DialWhatsAppCallRequest", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DeleteAgentDispatchRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DialWhatsAppCallResponse { +impl serde::Serialize for DeleteIngressRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -9157,39 +8962,30 @@ impl serde::Serialize for DialWhatsAppCallResponse { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.whatsapp_call_id.is_empty() { - len += 1; - } - if !self.room_name.is_empty() { + if !self.ingress_id.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.DialWhatsAppCallResponse", len)?; - if !self.whatsapp_call_id.is_empty() { - struct_ser.serialize_field("whatsappCallId", &self.whatsapp_call_id)?; - } - if !self.room_name.is_empty() { - struct_ser.serialize_field("roomName", &self.room_name)?; + let mut struct_ser = serializer.serialize_struct("livekit.DeleteIngressRequest", len)?; + if !self.ingress_id.is_empty() { + struct_ser.serialize_field("ingressId", &self.ingress_id)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DialWhatsAppCallResponse { +impl<'de> serde::Deserialize<'de> for DeleteIngressRequest { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "whatsapp_call_id", - "whatsappCallId", - "room_name", - "roomName", + "ingress_id", + "ingressId", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - WhatsappCallId, - RoomName, + IngressId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -9212,8 +9008,7 @@ impl<'de> serde::Deserialize<'de> for DialWhatsAppCallResponse { E: serde::de::Error, { match value { - "whatsappCallId" | "whatsapp_call_id" => Ok(GeneratedField::WhatsappCallId), - "roomName" | "room_name" => Ok(GeneratedField::RoomName), + "ingressId" | "ingress_id" => Ok(GeneratedField::IngressId), _ => Ok(GeneratedField::__SkipField__), } } @@ -9223,47 +9018,39 @@ impl<'de> serde::Deserialize<'de> for DialWhatsAppCallResponse { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DialWhatsAppCallResponse; + type Value = DeleteIngressRequest; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DialWhatsAppCallResponse") + formatter.write_str("struct livekit.DeleteIngressRequest") } - 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 whatsapp_call_id__ = None; - let mut room_name__ = None; + let mut ingress_id__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::WhatsappCallId => { - if whatsapp_call_id__.is_some() { - return Err(serde::de::Error::duplicate_field("whatsappCallId")); - } - whatsapp_call_id__ = Some(map_.next_value()?); - } - GeneratedField::RoomName => { - if room_name__.is_some() { - return Err(serde::de::Error::duplicate_field("roomName")); + GeneratedField::IngressId => { + if ingress_id__.is_some() { + return Err(serde::de::Error::duplicate_field("ingressId")); } - room_name__ = Some(map_.next_value()?); + ingress_id__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(DialWhatsAppCallResponse { - whatsapp_call_id: whatsapp_call_id__.unwrap_or_default(), - room_name: room_name__.unwrap_or_default(), + Ok(DeleteIngressRequest { + ingress_id: ingress_id__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.DialWhatsAppCallResponse", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DeleteIngressRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DirectFileOutput { +impl serde::Serialize for DeleteRoomRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -9271,65 +9058,29 @@ impl serde::Serialize for DirectFileOutput { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.filepath.is_empty() { - len += 1; - } - if self.disable_manifest { - len += 1; - } - if self.output.is_some() { + if !self.room.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.DirectFileOutput", len)?; - if !self.filepath.is_empty() { - struct_ser.serialize_field("filepath", &self.filepath)?; - } - if self.disable_manifest { - struct_ser.serialize_field("disableManifest", &self.disable_manifest)?; - } - if let Some(v) = self.output.as_ref() { - match v { - direct_file_output::Output::S3(v) => { - struct_ser.serialize_field("s3", v)?; - } - direct_file_output::Output::Gcp(v) => { - struct_ser.serialize_field("gcp", v)?; - } - direct_file_output::Output::Azure(v) => { - struct_ser.serialize_field("azure", v)?; - } - direct_file_output::Output::AliOss(v) => { - struct_ser.serialize_field("aliOSS", v)?; - } - } + let mut struct_ser = serializer.serialize_struct("livekit.DeleteRoomRequest", len)?; + if !self.room.is_empty() { + struct_ser.serialize_field("room", &self.room)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DirectFileOutput { +impl<'de> serde::Deserialize<'de> for DeleteRoomRequest { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "filepath", - "disable_manifest", - "disableManifest", - "s3", - "gcp", - "azure", - "aliOSS", + "room", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Filepath, - DisableManifest, - S3, - Gcp, - Azure, - AliOss, + Room, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -9352,12 +9103,7 @@ impl<'de> serde::Deserialize<'de> for DirectFileOutput { E: serde::de::Error, { match value { - "filepath" => Ok(GeneratedField::Filepath), - "disableManifest" | "disable_manifest" => Ok(GeneratedField::DisableManifest), - "s3" => Ok(GeneratedField::S3), - "gcp" => Ok(GeneratedField::Gcp), - "azure" => Ok(GeneratedField::Azure), - "aliOSS" => Ok(GeneratedField::AliOss), + "room" => Ok(GeneratedField::Room), _ => Ok(GeneratedField::__SkipField__), } } @@ -9367,115 +9113,61 @@ impl<'de> serde::Deserialize<'de> for DirectFileOutput { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DirectFileOutput; + type Value = DeleteRoomRequest; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DirectFileOutput") + formatter.write_str("struct livekit.DeleteRoomRequest") } - 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 filepath__ = None; - let mut disable_manifest__ = None; - let mut output__ = None; + let mut room__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Filepath => { - if filepath__.is_some() { - return Err(serde::de::Error::duplicate_field("filepath")); - } - filepath__ = Some(map_.next_value()?); - } - GeneratedField::DisableManifest => { - if disable_manifest__.is_some() { - return Err(serde::de::Error::duplicate_field("disableManifest")); - } - disable_manifest__ = Some(map_.next_value()?); - } - GeneratedField::S3 => { - if output__.is_some() { - return Err(serde::de::Error::duplicate_field("s3")); - } - output__ = map_.next_value::<::std::option::Option<_>>()?.map(direct_file_output::Output::S3) -; - } - GeneratedField::Gcp => { - if output__.is_some() { - return Err(serde::de::Error::duplicate_field("gcp")); - } - output__ = map_.next_value::<::std::option::Option<_>>()?.map(direct_file_output::Output::Gcp) -; - } - GeneratedField::Azure => { - if output__.is_some() { - return Err(serde::de::Error::duplicate_field("azure")); - } - output__ = map_.next_value::<::std::option::Option<_>>()?.map(direct_file_output::Output::Azure) -; - } - GeneratedField::AliOss => { - if output__.is_some() { - return Err(serde::de::Error::duplicate_field("aliOSS")); + GeneratedField::Room => { + if room__.is_some() { + return Err(serde::de::Error::duplicate_field("room")); } - output__ = map_.next_value::<::std::option::Option<_>>()?.map(direct_file_output::Output::AliOss) -; + room__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(DirectFileOutput { - filepath: filepath__.unwrap_or_default(), - disable_manifest: disable_manifest__.unwrap_or_default(), - output: output__, + Ok(DeleteRoomRequest { + room: room__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.DirectFileOutput", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DeleteRoomRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DisabledCodecs { +impl serde::Serialize for DeleteRoomResponse { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { use serde::ser::SerializeStruct; - let mut len = 0; - if !self.codecs.is_empty() { - len += 1; - } - if !self.publish.is_empty() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.DisabledCodecs", len)?; - if !self.codecs.is_empty() { - struct_ser.serialize_field("codecs", &self.codecs)?; - } - if !self.publish.is_empty() { - struct_ser.serialize_field("publish", &self.publish)?; - } + let len = 0; + let struct_ser = serializer.serialize_struct("livekit.DeleteRoomResponse", len)?; struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DisabledCodecs { +impl<'de> serde::Deserialize<'de> for DeleteRoomResponse { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "codecs", - "publish", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Codecs, - Publish, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -9497,11 +9189,7 @@ impl<'de> serde::Deserialize<'de> for DisabledCodecs { where E: serde::de::Error, { - match value { - "codecs" => Ok(GeneratedField::Codecs), - "publish" => Ok(GeneratedField::Publish), - _ => Ok(GeneratedField::__SkipField__), - } + Ok(GeneratedField::__SkipField__) } } deserializer.deserialize_identifier(GeneratedVisitor) @@ -9509,160 +9197,123 @@ impl<'de> serde::Deserialize<'de> for DisabledCodecs { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DisabledCodecs; + type Value = DeleteRoomResponse; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DisabledCodecs") + formatter.write_str("struct livekit.DeleteRoomResponse") } - 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 codecs__ = None; - let mut publish__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::Codecs => { - if codecs__.is_some() { - return Err(serde::de::Error::duplicate_field("codecs")); - } - codecs__ = Some(map_.next_value()?); - } - GeneratedField::Publish => { - if publish__.is_some() { - return Err(serde::de::Error::duplicate_field("publish")); - } - publish__ = Some(map_.next_value()?); - } - GeneratedField::__SkipField__ => { - let _ = map_.next_value::()?; - } - } + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; } - Ok(DisabledCodecs { - codecs: codecs__.unwrap_or_default(), - publish: publish__.unwrap_or_default(), + Ok(DeleteRoomResponse { }) } } - deserializer.deserialize_struct("livekit.DisabledCodecs", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DeleteRoomResponse", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DisconnectReason { +impl serde::Serialize for DeleteSipDispatchRuleRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { - let variant = match self { - Self::UnknownReason => "UNKNOWN_REASON", - Self::ClientInitiated => "CLIENT_INITIATED", - Self::DuplicateIdentity => "DUPLICATE_IDENTITY", - Self::ServerShutdown => "SERVER_SHUTDOWN", - Self::ParticipantRemoved => "PARTICIPANT_REMOVED", - Self::RoomDeleted => "ROOM_DELETED", - Self::StateMismatch => "STATE_MISMATCH", - Self::JoinFailure => "JOIN_FAILURE", - Self::Migration => "MIGRATION", - Self::SignalClose => "SIGNAL_CLOSE", - Self::RoomClosed => "ROOM_CLOSED", - Self::UserUnavailable => "USER_UNAVAILABLE", - Self::UserRejected => "USER_REJECTED", - Self::SipTrunkFailure => "SIP_TRUNK_FAILURE", - Self::ConnectionTimeout => "CONNECTION_TIMEOUT", - Self::MediaFailure => "MEDIA_FAILURE", - }; - serializer.serialize_str(variant) + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.sip_dispatch_rule_id.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DeleteSIPDispatchRuleRequest", len)?; + if !self.sip_dispatch_rule_id.is_empty() { + struct_ser.serialize_field("sipDispatchRuleId", &self.sip_dispatch_rule_id)?; + } + struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DisconnectReason { +impl<'de> serde::Deserialize<'de> for DeleteSipDispatchRuleRequest { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "UNKNOWN_REASON", - "CLIENT_INITIATED", - "DUPLICATE_IDENTITY", - "SERVER_SHUTDOWN", - "PARTICIPANT_REMOVED", - "ROOM_DELETED", - "STATE_MISMATCH", - "JOIN_FAILURE", - "MIGRATION", - "SIGNAL_CLOSE", - "ROOM_CLOSED", - "USER_UNAVAILABLE", - "USER_REJECTED", - "SIP_TRUNK_FAILURE", - "CONNECTION_TIMEOUT", - "MEDIA_FAILURE", + "sip_dispatch_rule_id", + "sipDispatchRuleId", ]; - struct GeneratedVisitor; + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + SipDispatchRuleId, + __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 = DisconnectReason; + 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) - } + 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) - }) + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "sipDispatchRuleId" | "sip_dispatch_rule_id" => Ok(GeneratedField::SipDispatchRuleId), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DeleteSipDispatchRuleRequest; - 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 expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DeleteSIPDispatchRuleRequest") } - fn visit_str(self, value: &str) -> std::result::Result - where - E: serde::de::Error, + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, { - match value { - "UNKNOWN_REASON" => Ok(DisconnectReason::UnknownReason), - "CLIENT_INITIATED" => Ok(DisconnectReason::ClientInitiated), - "DUPLICATE_IDENTITY" => Ok(DisconnectReason::DuplicateIdentity), - "SERVER_SHUTDOWN" => Ok(DisconnectReason::ServerShutdown), - "PARTICIPANT_REMOVED" => Ok(DisconnectReason::ParticipantRemoved), - "ROOM_DELETED" => Ok(DisconnectReason::RoomDeleted), - "STATE_MISMATCH" => Ok(DisconnectReason::StateMismatch), - "JOIN_FAILURE" => Ok(DisconnectReason::JoinFailure), - "MIGRATION" => Ok(DisconnectReason::Migration), - "SIGNAL_CLOSE" => Ok(DisconnectReason::SignalClose), - "ROOM_CLOSED" => Ok(DisconnectReason::RoomClosed), - "USER_UNAVAILABLE" => Ok(DisconnectReason::UserUnavailable), - "USER_REJECTED" => Ok(DisconnectReason::UserRejected), - "SIP_TRUNK_FAILURE" => Ok(DisconnectReason::SipTrunkFailure), - "CONNECTION_TIMEOUT" => Ok(DisconnectReason::ConnectionTimeout), - "MEDIA_FAILURE" => Ok(DisconnectReason::MediaFailure), - _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + let mut sip_dispatch_rule_id__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::SipDispatchRuleId => { + if sip_dispatch_rule_id__.is_some() { + return Err(serde::de::Error::duplicate_field("sipDispatchRuleId")); + } + sip_dispatch_rule_id__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } } + Ok(DeleteSipDispatchRuleRequest { + sip_dispatch_rule_id: sip_dispatch_rule_id__.unwrap_or_default(), + }) } } - deserializer.deserialize_any(GeneratedVisitor) + deserializer.deserialize_struct("livekit.DeleteSIPDispatchRuleRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DisconnectWhatsAppCallRequest { +impl serde::Serialize for DeleteSipTrunkRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -9670,39 +9321,30 @@ impl serde::Serialize for DisconnectWhatsAppCallRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.whatsapp_call_id.is_empty() { - len += 1; - } - if !self.whatsapp_api_key.is_empty() { + if !self.sip_trunk_id.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.DisconnectWhatsAppCallRequest", len)?; - if !self.whatsapp_call_id.is_empty() { - struct_ser.serialize_field("whatsappCallId", &self.whatsapp_call_id)?; - } - if !self.whatsapp_api_key.is_empty() { - struct_ser.serialize_field("whatsappApiKey", &self.whatsapp_api_key)?; + let mut struct_ser = serializer.serialize_struct("livekit.DeleteSIPTrunkRequest", len)?; + if !self.sip_trunk_id.is_empty() { + struct_ser.serialize_field("sipTrunkId", &self.sip_trunk_id)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallRequest { +impl<'de> serde::Deserialize<'de> for DeleteSipTrunkRequest { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "whatsapp_call_id", - "whatsappCallId", - "whatsapp_api_key", - "whatsappApiKey", + "sip_trunk_id", + "sipTrunkId", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - WhatsappCallId, - WhatsappApiKey, + SipTrunkId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -9725,8 +9367,7 @@ impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallRequest { E: serde::de::Error, { match value { - "whatsappCallId" | "whatsapp_call_id" => Ok(GeneratedField::WhatsappCallId), - "whatsappApiKey" | "whatsapp_api_key" => Ok(GeneratedField::WhatsappApiKey), + "sipTrunkId" | "sip_trunk_id" => Ok(GeneratedField::SipTrunkId), _ => Ok(GeneratedField::__SkipField__), } } @@ -9736,69 +9377,85 @@ impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallRequest { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DisconnectWhatsAppCallRequest; + type Value = DeleteSipTrunkRequest; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DisconnectWhatsAppCallRequest") + formatter.write_str("struct livekit.DeleteSIPTrunkRequest") } - 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 whatsapp_call_id__ = None; - let mut whatsapp_api_key__ = None; + let mut sip_trunk_id__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::WhatsappCallId => { - if whatsapp_call_id__.is_some() { - return Err(serde::de::Error::duplicate_field("whatsappCallId")); - } - whatsapp_call_id__ = Some(map_.next_value()?); - } - GeneratedField::WhatsappApiKey => { - if whatsapp_api_key__.is_some() { - return Err(serde::de::Error::duplicate_field("whatsappApiKey")); + GeneratedField::SipTrunkId => { + if sip_trunk_id__.is_some() { + return Err(serde::de::Error::duplicate_field("sipTrunkId")); } - whatsapp_api_key__ = Some(map_.next_value()?); + sip_trunk_id__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(DisconnectWhatsAppCallRequest { - whatsapp_call_id: whatsapp_call_id__.unwrap_or_default(), - whatsapp_api_key: whatsapp_api_key__.unwrap_or_default(), + Ok(DeleteSipTrunkRequest { + sip_trunk_id: sip_trunk_id__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.DisconnectWhatsAppCallRequest", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DeleteSIPTrunkRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DisconnectWhatsAppCallResponse { +impl serde::Serialize for Destination { #[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.DisconnectWhatsAppCallResponse", len)?; + let mut len = 0; + if !self.city.is_empty() { + len += 1; + } + if !self.country.is_empty() { + len += 1; + } + if !self.region.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.Destination", len)?; + if !self.city.is_empty() { + struct_ser.serialize_field("city", &self.city)?; + } + if !self.country.is_empty() { + struct_ser.serialize_field("country", &self.country)?; + } + if !self.region.is_empty() { + struct_ser.serialize_field("region", &self.region)?; + } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallResponse { +impl<'de> serde::Deserialize<'de> for Destination { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ + "city", + "country", + "region", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { + City, + Country, + Region, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -9820,7 +9477,12 @@ impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallResponse { where E: serde::de::Error, { - Ok(GeneratedField::__SkipField__) + match value { + "city" => Ok(GeneratedField::City), + "country" => Ok(GeneratedField::Country), + "region" => Ok(GeneratedField::Region), + _ => Ok(GeneratedField::__SkipField__), + } } } deserializer.deserialize_identifier(GeneratedVisitor) @@ -9828,27 +9490,55 @@ impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallResponse { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DisconnectWhatsAppCallResponse; + type Value = Destination; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DisconnectWhatsAppCallResponse") + formatter.write_str("struct livekit.Destination") } - 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 city__ = None; + let mut country__ = None; + let mut region__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::City => { + if city__.is_some() { + return Err(serde::de::Error::duplicate_field("city")); + } + city__ = Some(map_.next_value()?); + } + GeneratedField::Country => { + if country__.is_some() { + return Err(serde::de::Error::duplicate_field("country")); + } + country__ = Some(map_.next_value()?); + } + GeneratedField::Region => { + if region__.is_some() { + return Err(serde::de::Error::duplicate_field("region")); + } + region__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } } - Ok(DisconnectWhatsAppCallResponse { + Ok(Destination { + city: city__.unwrap_or_default(), + country: country__.unwrap_or_default(), + region: region__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.DisconnectWhatsAppCallResponse", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.Destination", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for EgressInfo { +impl serde::Serialize for DialWhatsAppCallRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -9856,64 +9546,1149 @@ impl serde::Serialize for EgressInfo { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.egress_id.is_empty() { + if !self.whatsapp_phone_number_id.is_empty() { len += 1; } - if !self.room_id.is_empty() { + if !self.whatsapp_to_phone_number.is_empty() { len += 1; } - if !self.room_name.is_empty() { + if !self.whatsapp_api_key.is_empty() { len += 1; } - if self.source_type != 0 { + if !self.whatsapp_cloud_api_version.is_empty() { len += 1; } - if self.status != 0 { + if !self.whatsapp_biz_opaque_callback_data.is_empty() { len += 1; } - if self.started_at != 0 { + if !self.room_name.is_empty() { len += 1; } - if self.ended_at != 0 { + if !self.agents.is_empty() { len += 1; } - if self.updated_at != 0 { + if !self.participant_identity.is_empty() { len += 1; } - if !self.details.is_empty() { + if !self.participant_name.is_empty() { len += 1; } - if !self.error.is_empty() { + if !self.participant_metadata.is_empty() { len += 1; } - if self.error_code != 0 { + if !self.participant_attributes.is_empty() { len += 1; } - if !self.stream_results.is_empty() { + if !self.destination_country.is_empty() { len += 1; } - if !self.file_results.is_empty() { - len += 1; + let mut struct_ser = serializer.serialize_struct("livekit.DialWhatsAppCallRequest", len)?; + if !self.whatsapp_phone_number_id.is_empty() { + struct_ser.serialize_field("whatsappPhoneNumberId", &self.whatsapp_phone_number_id)?; } - if !self.segment_results.is_empty() { - len += 1; + if !self.whatsapp_to_phone_number.is_empty() { + struct_ser.serialize_field("whatsappToPhoneNumber", &self.whatsapp_to_phone_number)?; } - if !self.image_results.is_empty() { - len += 1; + if !self.whatsapp_api_key.is_empty() { + struct_ser.serialize_field("whatsappApiKey", &self.whatsapp_api_key)?; } - if !self.manifest_location.is_empty() { - len += 1; + if !self.whatsapp_cloud_api_version.is_empty() { + struct_ser.serialize_field("whatsappCloudApiVersion", &self.whatsapp_cloud_api_version)?; } - if self.backup_storage_used { - len += 1; + if !self.whatsapp_biz_opaque_callback_data.is_empty() { + struct_ser.serialize_field("whatsappBizOpaqueCallbackData", &self.whatsapp_biz_opaque_callback_data)?; } - if self.request.is_some() { - len += 1; + if !self.room_name.is_empty() { + struct_ser.serialize_field("roomName", &self.room_name)?; } - if self.result.is_some() { - len += 1; + if !self.agents.is_empty() { + struct_ser.serialize_field("agents", &self.agents)?; } - let mut struct_ser = serializer.serialize_struct("livekit.EgressInfo", len)?; + if !self.participant_identity.is_empty() { + struct_ser.serialize_field("participantIdentity", &self.participant_identity)?; + } + if !self.participant_name.is_empty() { + struct_ser.serialize_field("participantName", &self.participant_name)?; + } + if !self.participant_metadata.is_empty() { + struct_ser.serialize_field("participantMetadata", &self.participant_metadata)?; + } + if !self.participant_attributes.is_empty() { + struct_ser.serialize_field("participantAttributes", &self.participant_attributes)?; + } + if !self.destination_country.is_empty() { + struct_ser.serialize_field("destinationCountry", &self.destination_country)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DialWhatsAppCallRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "whatsapp_phone_number_id", + "whatsappPhoneNumberId", + "whatsapp_to_phone_number", + "whatsappToPhoneNumber", + "whatsapp_api_key", + "whatsappApiKey", + "whatsapp_cloud_api_version", + "whatsappCloudApiVersion", + "whatsapp_biz_opaque_callback_data", + "whatsappBizOpaqueCallbackData", + "room_name", + "roomName", + "agents", + "participant_identity", + "participantIdentity", + "participant_name", + "participantName", + "participant_metadata", + "participantMetadata", + "participant_attributes", + "participantAttributes", + "destination_country", + "destinationCountry", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + WhatsappPhoneNumberId, + WhatsappToPhoneNumber, + WhatsappApiKey, + WhatsappCloudApiVersion, + WhatsappBizOpaqueCallbackData, + RoomName, + Agents, + ParticipantIdentity, + ParticipantName, + ParticipantMetadata, + ParticipantAttributes, + DestinationCountry, + __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 { + "whatsappPhoneNumberId" | "whatsapp_phone_number_id" => Ok(GeneratedField::WhatsappPhoneNumberId), + "whatsappToPhoneNumber" | "whatsapp_to_phone_number" => Ok(GeneratedField::WhatsappToPhoneNumber), + "whatsappApiKey" | "whatsapp_api_key" => Ok(GeneratedField::WhatsappApiKey), + "whatsappCloudApiVersion" | "whatsapp_cloud_api_version" => Ok(GeneratedField::WhatsappCloudApiVersion), + "whatsappBizOpaqueCallbackData" | "whatsapp_biz_opaque_callback_data" => Ok(GeneratedField::WhatsappBizOpaqueCallbackData), + "roomName" | "room_name" => Ok(GeneratedField::RoomName), + "agents" => Ok(GeneratedField::Agents), + "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), + "participantName" | "participant_name" => Ok(GeneratedField::ParticipantName), + "participantMetadata" | "participant_metadata" => Ok(GeneratedField::ParticipantMetadata), + "participantAttributes" | "participant_attributes" => Ok(GeneratedField::ParticipantAttributes), + "destinationCountry" | "destination_country" => Ok(GeneratedField::DestinationCountry), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DialWhatsAppCallRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DialWhatsAppCallRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut whatsapp_phone_number_id__ = None; + let mut whatsapp_to_phone_number__ = None; + let mut whatsapp_api_key__ = None; + let mut whatsapp_cloud_api_version__ = None; + let mut whatsapp_biz_opaque_callback_data__ = None; + let mut room_name__ = None; + let mut agents__ = None; + let mut participant_identity__ = None; + let mut participant_name__ = None; + let mut participant_metadata__ = None; + let mut participant_attributes__ = None; + let mut destination_country__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::WhatsappPhoneNumberId => { + if whatsapp_phone_number_id__.is_some() { + return Err(serde::de::Error::duplicate_field("whatsappPhoneNumberId")); + } + whatsapp_phone_number_id__ = Some(map_.next_value()?); + } + GeneratedField::WhatsappToPhoneNumber => { + if whatsapp_to_phone_number__.is_some() { + return Err(serde::de::Error::duplicate_field("whatsappToPhoneNumber")); + } + whatsapp_to_phone_number__ = Some(map_.next_value()?); + } + GeneratedField::WhatsappApiKey => { + if whatsapp_api_key__.is_some() { + return Err(serde::de::Error::duplicate_field("whatsappApiKey")); + } + whatsapp_api_key__ = Some(map_.next_value()?); + } + GeneratedField::WhatsappCloudApiVersion => { + if whatsapp_cloud_api_version__.is_some() { + return Err(serde::de::Error::duplicate_field("whatsappCloudApiVersion")); + } + whatsapp_cloud_api_version__ = Some(map_.next_value()?); + } + GeneratedField::WhatsappBizOpaqueCallbackData => { + if whatsapp_biz_opaque_callback_data__.is_some() { + return Err(serde::de::Error::duplicate_field("whatsappBizOpaqueCallbackData")); + } + whatsapp_biz_opaque_callback_data__ = Some(map_.next_value()?); + } + GeneratedField::RoomName => { + if room_name__.is_some() { + return Err(serde::de::Error::duplicate_field("roomName")); + } + room_name__ = Some(map_.next_value()?); + } + GeneratedField::Agents => { + if agents__.is_some() { + return Err(serde::de::Error::duplicate_field("agents")); + } + agents__ = Some(map_.next_value()?); + } + GeneratedField::ParticipantIdentity => { + if participant_identity__.is_some() { + return Err(serde::de::Error::duplicate_field("participantIdentity")); + } + participant_identity__ = Some(map_.next_value()?); + } + GeneratedField::ParticipantName => { + if participant_name__.is_some() { + return Err(serde::de::Error::duplicate_field("participantName")); + } + participant_name__ = Some(map_.next_value()?); + } + GeneratedField::ParticipantMetadata => { + if participant_metadata__.is_some() { + return Err(serde::de::Error::duplicate_field("participantMetadata")); + } + participant_metadata__ = Some(map_.next_value()?); + } + GeneratedField::ParticipantAttributes => { + if participant_attributes__.is_some() { + return Err(serde::de::Error::duplicate_field("participantAttributes")); + } + participant_attributes__ = Some( + map_.next_value::>()? + ); + } + GeneratedField::DestinationCountry => { + if destination_country__.is_some() { + return Err(serde::de::Error::duplicate_field("destinationCountry")); + } + destination_country__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DialWhatsAppCallRequest { + whatsapp_phone_number_id: whatsapp_phone_number_id__.unwrap_or_default(), + whatsapp_to_phone_number: whatsapp_to_phone_number__.unwrap_or_default(), + whatsapp_api_key: whatsapp_api_key__.unwrap_or_default(), + whatsapp_cloud_api_version: whatsapp_cloud_api_version__.unwrap_or_default(), + whatsapp_biz_opaque_callback_data: whatsapp_biz_opaque_callback_data__.unwrap_or_default(), + room_name: room_name__.unwrap_or_default(), + agents: agents__.unwrap_or_default(), + participant_identity: participant_identity__.unwrap_or_default(), + participant_name: participant_name__.unwrap_or_default(), + participant_metadata: participant_metadata__.unwrap_or_default(), + participant_attributes: participant_attributes__.unwrap_or_default(), + destination_country: destination_country__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DialWhatsAppCallRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DialWhatsAppCallResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.whatsapp_call_id.is_empty() { + len += 1; + } + if !self.room_name.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DialWhatsAppCallResponse", len)?; + if !self.whatsapp_call_id.is_empty() { + struct_ser.serialize_field("whatsappCallId", &self.whatsapp_call_id)?; + } + if !self.room_name.is_empty() { + struct_ser.serialize_field("roomName", &self.room_name)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DialWhatsAppCallResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "whatsapp_call_id", + "whatsappCallId", + "room_name", + "roomName", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + WhatsappCallId, + RoomName, + __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 { + "whatsappCallId" | "whatsapp_call_id" => Ok(GeneratedField::WhatsappCallId), + "roomName" | "room_name" => Ok(GeneratedField::RoomName), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DialWhatsAppCallResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DialWhatsAppCallResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut whatsapp_call_id__ = None; + let mut room_name__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::WhatsappCallId => { + if whatsapp_call_id__.is_some() { + return Err(serde::de::Error::duplicate_field("whatsappCallId")); + } + whatsapp_call_id__ = Some(map_.next_value()?); + } + GeneratedField::RoomName => { + if room_name__.is_some() { + return Err(serde::de::Error::duplicate_field("roomName")); + } + room_name__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DialWhatsAppCallResponse { + whatsapp_call_id: whatsapp_call_id__.unwrap_or_default(), + room_name: room_name__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DialWhatsAppCallResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DirectFileOutput { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.filepath.is_empty() { + len += 1; + } + if self.disable_manifest { + len += 1; + } + if self.output.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DirectFileOutput", len)?; + if !self.filepath.is_empty() { + struct_ser.serialize_field("filepath", &self.filepath)?; + } + if self.disable_manifest { + struct_ser.serialize_field("disableManifest", &self.disable_manifest)?; + } + if let Some(v) = self.output.as_ref() { + match v { + direct_file_output::Output::S3(v) => { + struct_ser.serialize_field("s3", v)?; + } + direct_file_output::Output::Gcp(v) => { + struct_ser.serialize_field("gcp", v)?; + } + direct_file_output::Output::Azure(v) => { + struct_ser.serialize_field("azure", v)?; + } + direct_file_output::Output::AliOss(v) => { + struct_ser.serialize_field("aliOSS", v)?; + } + } + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DirectFileOutput { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "filepath", + "disable_manifest", + "disableManifest", + "s3", + "gcp", + "azure", + "aliOSS", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Filepath, + DisableManifest, + S3, + Gcp, + Azure, + AliOss, + __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 { + "filepath" => Ok(GeneratedField::Filepath), + "disableManifest" | "disable_manifest" => Ok(GeneratedField::DisableManifest), + "s3" => Ok(GeneratedField::S3), + "gcp" => Ok(GeneratedField::Gcp), + "azure" => Ok(GeneratedField::Azure), + "aliOSS" => Ok(GeneratedField::AliOss), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DirectFileOutput; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DirectFileOutput") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut filepath__ = None; + let mut disable_manifest__ = None; + let mut output__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Filepath => { + if filepath__.is_some() { + return Err(serde::de::Error::duplicate_field("filepath")); + } + filepath__ = Some(map_.next_value()?); + } + GeneratedField::DisableManifest => { + if disable_manifest__.is_some() { + return Err(serde::de::Error::duplicate_field("disableManifest")); + } + disable_manifest__ = Some(map_.next_value()?); + } + GeneratedField::S3 => { + if output__.is_some() { + return Err(serde::de::Error::duplicate_field("s3")); + } + output__ = map_.next_value::<::std::option::Option<_>>()?.map(direct_file_output::Output::S3) +; + } + GeneratedField::Gcp => { + if output__.is_some() { + return Err(serde::de::Error::duplicate_field("gcp")); + } + output__ = map_.next_value::<::std::option::Option<_>>()?.map(direct_file_output::Output::Gcp) +; + } + GeneratedField::Azure => { + if output__.is_some() { + return Err(serde::de::Error::duplicate_field("azure")); + } + output__ = map_.next_value::<::std::option::Option<_>>()?.map(direct_file_output::Output::Azure) +; + } + GeneratedField::AliOss => { + if output__.is_some() { + return Err(serde::de::Error::duplicate_field("aliOSS")); + } + output__ = map_.next_value::<::std::option::Option<_>>()?.map(direct_file_output::Output::AliOss) +; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DirectFileOutput { + filepath: filepath__.unwrap_or_default(), + disable_manifest: disable_manifest__.unwrap_or_default(), + output: output__, + }) + } + } + deserializer.deserialize_struct("livekit.DirectFileOutput", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DisabledCodecs { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.codecs.is_empty() { + len += 1; + } + if !self.publish.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DisabledCodecs", len)?; + if !self.codecs.is_empty() { + struct_ser.serialize_field("codecs", &self.codecs)?; + } + if !self.publish.is_empty() { + struct_ser.serialize_field("publish", &self.publish)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DisabledCodecs { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "codecs", + "publish", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Codecs, + Publish, + __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 { + "codecs" => Ok(GeneratedField::Codecs), + "publish" => Ok(GeneratedField::Publish), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DisabledCodecs; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DisabledCodecs") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut codecs__ = None; + let mut publish__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Codecs => { + if codecs__.is_some() { + return Err(serde::de::Error::duplicate_field("codecs")); + } + codecs__ = Some(map_.next_value()?); + } + GeneratedField::Publish => { + if publish__.is_some() { + return Err(serde::de::Error::duplicate_field("publish")); + } + publish__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DisabledCodecs { + codecs: codecs__.unwrap_or_default(), + publish: publish__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DisabledCodecs", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DisconnectReason { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::UnknownReason => "UNKNOWN_REASON", + Self::ClientInitiated => "CLIENT_INITIATED", + Self::DuplicateIdentity => "DUPLICATE_IDENTITY", + Self::ServerShutdown => "SERVER_SHUTDOWN", + Self::ParticipantRemoved => "PARTICIPANT_REMOVED", + Self::RoomDeleted => "ROOM_DELETED", + Self::StateMismatch => "STATE_MISMATCH", + Self::JoinFailure => "JOIN_FAILURE", + Self::Migration => "MIGRATION", + Self::SignalClose => "SIGNAL_CLOSE", + Self::RoomClosed => "ROOM_CLOSED", + Self::UserUnavailable => "USER_UNAVAILABLE", + Self::UserRejected => "USER_REJECTED", + Self::SipTrunkFailure => "SIP_TRUNK_FAILURE", + Self::ConnectionTimeout => "CONNECTION_TIMEOUT", + Self::MediaFailure => "MEDIA_FAILURE", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for DisconnectReason { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "UNKNOWN_REASON", + "CLIENT_INITIATED", + "DUPLICATE_IDENTITY", + "SERVER_SHUTDOWN", + "PARTICIPANT_REMOVED", + "ROOM_DELETED", + "STATE_MISMATCH", + "JOIN_FAILURE", + "MIGRATION", + "SIGNAL_CLOSE", + "ROOM_CLOSED", + "USER_UNAVAILABLE", + "USER_REJECTED", + "SIP_TRUNK_FAILURE", + "CONNECTION_TIMEOUT", + "MEDIA_FAILURE", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DisconnectReason; + + 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 { + "UNKNOWN_REASON" => Ok(DisconnectReason::UnknownReason), + "CLIENT_INITIATED" => Ok(DisconnectReason::ClientInitiated), + "DUPLICATE_IDENTITY" => Ok(DisconnectReason::DuplicateIdentity), + "SERVER_SHUTDOWN" => Ok(DisconnectReason::ServerShutdown), + "PARTICIPANT_REMOVED" => Ok(DisconnectReason::ParticipantRemoved), + "ROOM_DELETED" => Ok(DisconnectReason::RoomDeleted), + "STATE_MISMATCH" => Ok(DisconnectReason::StateMismatch), + "JOIN_FAILURE" => Ok(DisconnectReason::JoinFailure), + "MIGRATION" => Ok(DisconnectReason::Migration), + "SIGNAL_CLOSE" => Ok(DisconnectReason::SignalClose), + "ROOM_CLOSED" => Ok(DisconnectReason::RoomClosed), + "USER_UNAVAILABLE" => Ok(DisconnectReason::UserUnavailable), + "USER_REJECTED" => Ok(DisconnectReason::UserRejected), + "SIP_TRUNK_FAILURE" => Ok(DisconnectReason::SipTrunkFailure), + "CONNECTION_TIMEOUT" => Ok(DisconnectReason::ConnectionTimeout), + "MEDIA_FAILURE" => Ok(DisconnectReason::MediaFailure), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for DisconnectWhatsAppCallRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.whatsapp_call_id.is_empty() { + len += 1; + } + if !self.whatsapp_api_key.is_empty() { + len += 1; + } + if self.disconnect_reason != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DisconnectWhatsAppCallRequest", len)?; + if !self.whatsapp_call_id.is_empty() { + struct_ser.serialize_field("whatsappCallId", &self.whatsapp_call_id)?; + } + if !self.whatsapp_api_key.is_empty() { + struct_ser.serialize_field("whatsappApiKey", &self.whatsapp_api_key)?; + } + if self.disconnect_reason != 0 { + let v = disconnect_whats_app_call_request::DisconnectReason::try_from(self.disconnect_reason) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.disconnect_reason)))?; + struct_ser.serialize_field("disconnectReason", &v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "whatsapp_call_id", + "whatsappCallId", + "whatsapp_api_key", + "whatsappApiKey", + "disconnect_reason", + "disconnectReason", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + WhatsappCallId, + WhatsappApiKey, + DisconnectReason, + __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 { + "whatsappCallId" | "whatsapp_call_id" => Ok(GeneratedField::WhatsappCallId), + "whatsappApiKey" | "whatsapp_api_key" => Ok(GeneratedField::WhatsappApiKey), + "disconnectReason" | "disconnect_reason" => Ok(GeneratedField::DisconnectReason), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DisconnectWhatsAppCallRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DisconnectWhatsAppCallRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut whatsapp_call_id__ = None; + let mut whatsapp_api_key__ = None; + let mut disconnect_reason__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::WhatsappCallId => { + if whatsapp_call_id__.is_some() { + return Err(serde::de::Error::duplicate_field("whatsappCallId")); + } + whatsapp_call_id__ = Some(map_.next_value()?); + } + GeneratedField::WhatsappApiKey => { + if whatsapp_api_key__.is_some() { + return Err(serde::de::Error::duplicate_field("whatsappApiKey")); + } + whatsapp_api_key__ = Some(map_.next_value()?); + } + GeneratedField::DisconnectReason => { + if disconnect_reason__.is_some() { + return Err(serde::de::Error::duplicate_field("disconnectReason")); + } + disconnect_reason__ = Some(map_.next_value::()? as i32); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DisconnectWhatsAppCallRequest { + whatsapp_call_id: whatsapp_call_id__.unwrap_or_default(), + whatsapp_api_key: whatsapp_api_key__.unwrap_or_default(), + disconnect_reason: disconnect_reason__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DisconnectWhatsAppCallRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for disconnect_whats_app_call_request::DisconnectReason { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::BusinessInitiated => "BUSINESS_INITIATED", + Self::UserInitiated => "USER_INITIATED", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for disconnect_whats_app_call_request::DisconnectReason { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "BUSINESS_INITIATED", + "USER_INITIATED", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = disconnect_whats_app_call_request::DisconnectReason; + + 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 { + "BUSINESS_INITIATED" => Ok(disconnect_whats_app_call_request::DisconnectReason::BusinessInitiated), + "USER_INITIATED" => Ok(disconnect_whats_app_call_request::DisconnectReason::UserInitiated), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for DisconnectWhatsAppCallResponse { + #[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.DisconnectWhatsAppCallResponse", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallResponse { + #[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 = DisconnectWhatsAppCallResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DisconnectWhatsAppCallResponse") + } + + 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(DisconnectWhatsAppCallResponse { + }) + } + } + deserializer.deserialize_struct("livekit.DisconnectWhatsAppCallResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for EgressInfo { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.egress_id.is_empty() { + len += 1; + } + if !self.room_id.is_empty() { + len += 1; + } + if !self.room_name.is_empty() { + len += 1; + } + if self.source_type != 0 { + len += 1; + } + if self.status != 0 { + len += 1; + } + if self.started_at != 0 { + len += 1; + } + if self.ended_at != 0 { + len += 1; + } + if self.updated_at != 0 { + len += 1; + } + if !self.details.is_empty() { + len += 1; + } + if !self.error.is_empty() { + len += 1; + } + if self.error_code != 0 { + len += 1; + } + if !self.stream_results.is_empty() { + len += 1; + } + if !self.file_results.is_empty() { + len += 1; + } + if !self.segment_results.is_empty() { + len += 1; + } + if !self.image_results.is_empty() { + len += 1; + } + if !self.manifest_location.is_empty() { + len += 1; + } + if self.backup_storage_used { + len += 1; + } + if self.request.is_some() { + len += 1; + } + if self.result.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.EgressInfo", len)?; if !self.egress_id.is_empty() { struct_ser.serialize_field("egressId", &self.egress_id)?; } @@ -19868,6 +20643,9 @@ impl serde::Serialize for MetricsRecordingHeader { if self.start_time.is_some() { len += 1; } + if !self.room_tags.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.MetricsRecordingHeader", len)?; if !self.room_id.is_empty() { struct_ser.serialize_field("roomId", &self.room_id)?; @@ -19880,6 +20658,9 @@ impl serde::Serialize for MetricsRecordingHeader { if let Some(v) = self.start_time.as_ref() { struct_ser.serialize_field("startTime", v)?; } + if !self.room_tags.is_empty() { + struct_ser.serialize_field("roomTags", &self.room_tags)?; + } struct_ser.end() } } @@ -19895,6 +20676,8 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { "duration", "start_time", "startTime", + "room_tags", + "roomTags", ]; #[allow(clippy::enum_variant_names)] @@ -19902,6 +20685,7 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { RoomId, Duration, StartTime, + RoomTags, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -19927,6 +20711,7 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { "roomId" | "room_id" => Ok(GeneratedField::RoomId), "duration" => Ok(GeneratedField::Duration), "startTime" | "start_time" => Ok(GeneratedField::StartTime), + "roomTags" | "room_tags" => Ok(GeneratedField::RoomTags), _ => Ok(GeneratedField::__SkipField__), } } @@ -19949,6 +20734,7 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { let mut room_id__ = None; let mut duration__ = None; let mut start_time__ = None; + let mut room_tags__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::RoomId => { @@ -19971,6 +20757,14 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { } start_time__ = map_.next_value()?; } + GeneratedField::RoomTags => { + if room_tags__.is_some() { + return Err(serde::de::Error::duplicate_field("roomTags")); + } + room_tags__ = Some( + map_.next_value::>()? + ); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -19980,6 +20774,7 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { room_id: room_id__.unwrap_or_default(), duration: duration__.unwrap_or_default(), start_time: start_time__, + room_tags: room_tags__.unwrap_or_default(), }) } } @@ -21065,6 +21860,9 @@ impl serde::Serialize for ParticipantInfo { if !self.kind_details.is_empty() { len += 1; } + if !self.data_tracks.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.ParticipantInfo", len)?; if !self.sid.is_empty() { struct_ser.serialize_field("sid", &self.sid)?; @@ -21128,6 +21926,9 @@ impl serde::Serialize for ParticipantInfo { }).collect::, _>>()?; struct_ser.serialize_field("kindDetails", &v)?; } + if !self.data_tracks.is_empty() { + struct_ser.serialize_field("dataTracks", &self.data_tracks)?; + } struct_ser.end() } } @@ -21159,6 +21960,8 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { "disconnectReason", "kind_details", "kindDetails", + "data_tracks", + "dataTracks", ]; #[allow(clippy::enum_variant_names)] @@ -21179,6 +21982,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { Attributes, DisconnectReason, KindDetails, + DataTracks, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -21217,6 +22021,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { "attributes" => Ok(GeneratedField::Attributes), "disconnectReason" | "disconnect_reason" => Ok(GeneratedField::DisconnectReason), "kindDetails" | "kind_details" => Ok(GeneratedField::KindDetails), + "dataTracks" | "data_tracks" => Ok(GeneratedField::DataTracks), _ => Ok(GeneratedField::__SkipField__), } } @@ -21252,6 +22057,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { let mut attributes__ = None; let mut disconnect_reason__ = None; let mut kind_details__ = None; + let mut data_tracks__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Sid => { @@ -21358,6 +22164,12 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { } kind_details__ = Some(map_.next_value::>()?.into_iter().map(|x| x as i32).collect()); } + GeneratedField::DataTracks => { + if data_tracks__.is_some() { + return Err(serde::de::Error::duplicate_field("dataTracks")); + } + data_tracks__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -21380,6 +22192,7 @@ impl<'de> serde::Deserialize<'de> for ParticipantInfo { attributes: attributes__.unwrap_or_default(), disconnect_reason: disconnect_reason__.unwrap_or_default(), kind_details: kind_details__.unwrap_or_default(), + data_tracks: data_tracks__.unwrap_or_default(), }) } } @@ -21399,6 +22212,7 @@ impl serde::Serialize for participant_info::Kind { Self::Sip => "SIP", Self::Agent => "AGENT", Self::Connector => "CONNECTOR", + Self::Bridge => "BRIDGE", }; serializer.serialize_str(variant) } @@ -21416,6 +22230,7 @@ impl<'de> serde::Deserialize<'de> for participant_info::Kind { "SIP", "AGENT", "CONNECTOR", + "BRIDGE", ]; struct GeneratedVisitor; @@ -21462,6 +22277,7 @@ impl<'de> serde::Deserialize<'de> for participant_info::Kind { "SIP" => Ok(participant_info::Kind::Sip), "AGENT" => Ok(participant_info::Kind::Agent), "CONNECTOR" => Ok(participant_info::Kind::Connector), + "BRIDGE" => Ok(participant_info::Kind::Bridge), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -21480,6 +22296,7 @@ impl serde::Serialize for participant_info::KindDetail { Self::Forwarded => "FORWARDED", Self::ConnectorWhatsapp => "CONNECTOR_WHATSAPP", Self::ConnectorTwilio => "CONNECTOR_TWILIO", + Self::BridgeRtsp => "BRIDGE_RTSP", }; serializer.serialize_str(variant) } @@ -21495,6 +22312,7 @@ impl<'de> serde::Deserialize<'de> for participant_info::KindDetail { "FORWARDED", "CONNECTOR_WHATSAPP", "CONNECTOR_TWILIO", + "BRIDGE_RTSP", ]; struct GeneratedVisitor; @@ -21539,6 +22357,7 @@ impl<'de> serde::Deserialize<'de> for participant_info::KindDetail { "FORWARDED" => Ok(participant_info::KindDetail::Forwarded), "CONNECTOR_WHATSAPP" => Ok(participant_info::KindDetail::ConnectorWhatsapp), "CONNECTOR_TWILIO" => Ok(participant_info::KindDetail::ConnectorTwilio), + "BRIDGE_RTSP" => Ok(participant_info::KindDetail::BridgeRtsp), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -22789,10 +23608,353 @@ impl<'de> serde::Deserialize<'de> for ProviderInfo { E: serde::de::Error, { match value { - "id" => Ok(GeneratedField::Id), + "id" => Ok(GeneratedField::Id), + "name" => Ok(GeneratedField::Name), + "type" => Ok(GeneratedField::Type), + "preventTransfer" | "prevent_transfer" => Ok(GeneratedField::PreventTransfer), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ProviderInfo; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.ProviderInfo") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut id__ = None; + let mut name__ = None; + let mut r#type__ = None; + let mut prevent_transfer__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id__ = Some(map_.next_value()?); + } + GeneratedField::Name => { + if name__.is_some() { + return Err(serde::de::Error::duplicate_field("name")); + } + name__ = Some(map_.next_value()?); + } + GeneratedField::Type => { + if r#type__.is_some() { + return Err(serde::de::Error::duplicate_field("type")); + } + r#type__ = Some(map_.next_value::()? as i32); + } + GeneratedField::PreventTransfer => { + if prevent_transfer__.is_some() { + return Err(serde::de::Error::duplicate_field("preventTransfer")); + } + prevent_transfer__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(ProviderInfo { + id: id__.unwrap_or_default(), + name: name__.unwrap_or_default(), + r#type: r#type__.unwrap_or_default(), + prevent_transfer: prevent_transfer__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.ProviderInfo", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ProviderType { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Unknown => "PROVIDER_TYPE_UNKNOWN", + Self::Internal => "PROVIDER_TYPE_INTERNAL", + Self::External => "PROVIDER_TYPE_EXTERNAL", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for ProviderType { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "PROVIDER_TYPE_UNKNOWN", + "PROVIDER_TYPE_INTERNAL", + "PROVIDER_TYPE_EXTERNAL", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ProviderType; + + 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 { + "PROVIDER_TYPE_UNKNOWN" => Ok(ProviderType::Unknown), + "PROVIDER_TYPE_INTERNAL" => Ok(ProviderType::Internal), + "PROVIDER_TYPE_EXTERNAL" => Ok(ProviderType::External), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for ProxyConfig { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.url.is_empty() { + len += 1; + } + if !self.username.is_empty() { + len += 1; + } + if !self.password.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.ProxyConfig", len)?; + if !self.url.is_empty() { + struct_ser.serialize_field("url", &self.url)?; + } + if !self.username.is_empty() { + struct_ser.serialize_field("username", &self.username)?; + } + if !self.password.is_empty() { + struct_ser.serialize_field("password", &self.password)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ProxyConfig { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "url", + "username", + "password", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Url, + Username, + Password, + __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 { + "url" => Ok(GeneratedField::Url), + "username" => Ok(GeneratedField::Username), + "password" => Ok(GeneratedField::Password), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ProxyConfig; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.ProxyConfig") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut url__ = None; + let mut username__ = None; + let mut password__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Url => { + if url__.is_some() { + return Err(serde::de::Error::duplicate_field("url")); + } + url__ = Some(map_.next_value()?); + } + GeneratedField::Username => { + if username__.is_some() { + return Err(serde::de::Error::duplicate_field("username")); + } + username__ = Some(map_.next_value()?); + } + GeneratedField::Password => { + if password__.is_some() { + return Err(serde::de::Error::duplicate_field("password")); + } + password__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(ProxyConfig { + url: url__.unwrap_or_default(), + username: username__.unwrap_or_default(), + password: password__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.ProxyConfig", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for PublishDataTrackRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.pub_handle != 0 { + len += 1; + } + if !self.name.is_empty() { + len += 1; + } + if self.encryption != 0 { + 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)?; + } + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + if self.encryption != 0 { + let v = encryption::Type::try_from(self.encryption) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; + struct_ser.serialize_field("encryption", &v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "pub_handle", + "pubHandle", + "name", + "encryption", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + PubHandle, + Name, + Encryption, + __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 { + "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), "name" => Ok(GeneratedField::Name), - "type" => Ok(GeneratedField::Type), - "preventTransfer" | "prevent_transfer" => Ok(GeneratedField::PreventTransfer), + "encryption" => Ok(GeneratedField::Encryption), _ => Ok(GeneratedField::__SkipField__), } } @@ -22802,27 +23964,28 @@ impl<'de> serde::Deserialize<'de> for ProviderInfo { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = ProviderInfo; + type Value = PublishDataTrackRequest; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.ProviderInfo") + formatter.write_str("struct livekit.PublishDataTrackRequest") } - 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 id__ = None; + let mut pub_handle__ = None; let mut name__ = None; - let mut r#type__ = None; - let mut prevent_transfer__ = None; + let mut encryption__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Id => { - if id__.is_some() { - return Err(serde::de::Error::duplicate_field("id")); + GeneratedField::PubHandle => { + if pub_handle__.is_some() { + return Err(serde::de::Error::duplicate_field("pubHandle")); } - id__ = Some(map_.next_value()?); + pub_handle__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; } GeneratedField::Name => { if name__.is_some() { @@ -22830,109 +23993,123 @@ impl<'de> serde::Deserialize<'de> for ProviderInfo { } name__ = Some(map_.next_value()?); } - GeneratedField::Type => { - if r#type__.is_some() { - return Err(serde::de::Error::duplicate_field("type")); - } - r#type__ = Some(map_.next_value::()? as i32); - } - GeneratedField::PreventTransfer => { - if prevent_transfer__.is_some() { - return Err(serde::de::Error::duplicate_field("preventTransfer")); + GeneratedField::Encryption => { + if encryption__.is_some() { + return Err(serde::de::Error::duplicate_field("encryption")); } - prevent_transfer__ = Some(map_.next_value()?); + encryption__ = Some(map_.next_value::()? as i32); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(ProviderInfo { - id: id__.unwrap_or_default(), + Ok(PublishDataTrackRequest { + pub_handle: pub_handle__.unwrap_or_default(), name: name__.unwrap_or_default(), - r#type: r#type__.unwrap_or_default(), - prevent_transfer: prevent_transfer__.unwrap_or_default(), + encryption: encryption__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.ProviderInfo", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.PublishDataTrackRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for ProviderType { +impl serde::Serialize for PublishDataTrackResponse { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { - let variant = match self { - Self::Unknown => "PROVIDER_TYPE_UNKNOWN", - Self::Internal => "PROVIDER_TYPE_INTERNAL", - Self::External => "PROVIDER_TYPE_EXTERNAL", - }; - serializer.serialize_str(variant) + use serde::ser::SerializeStruct; + let mut len = 0; + if self.info.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.PublishDataTrackResponse", len)?; + if let Some(v) = self.info.as_ref() { + struct_ser.serialize_field("info", v)?; + } + struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for ProviderType { +impl<'de> serde::Deserialize<'de> for PublishDataTrackResponse { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "PROVIDER_TYPE_UNKNOWN", - "PROVIDER_TYPE_INTERNAL", - "PROVIDER_TYPE_EXTERNAL", + "info", ]; - struct GeneratedVisitor; + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Info, + __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 = ProviderType; + 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) - } + 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) - }) + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "info" => Ok(GeneratedField::Info), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = PublishDataTrackResponse; - 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 expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.PublishDataTrackResponse") } - fn visit_str(self, value: &str) -> std::result::Result - where - E: serde::de::Error, + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, { - match value { - "PROVIDER_TYPE_UNKNOWN" => Ok(ProviderType::Unknown), - "PROVIDER_TYPE_INTERNAL" => Ok(ProviderType::Internal), - "PROVIDER_TYPE_EXTERNAL" => Ok(ProviderType::External), - _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + let mut info__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Info => { + if info__.is_some() { + return Err(serde::de::Error::duplicate_field("info")); + } + info__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } } + Ok(PublishDataTrackResponse { + info: info__, + }) } } - deserializer.deserialize_any(GeneratedVisitor) + deserializer.deserialize_struct("livekit.PublishDataTrackResponse", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for ProxyConfig { +impl serde::Serialize for PushTextRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -22940,45 +24117,38 @@ impl serde::Serialize for ProxyConfig { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.url.is_empty() { + if !self.message_id.is_empty() { len += 1; } - if !self.username.is_empty() { - len += 1; - } - if !self.password.is_empty() { + if !self.content.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.ProxyConfig", len)?; - if !self.url.is_empty() { - struct_ser.serialize_field("url", &self.url)?; + let mut struct_ser = serializer.serialize_struct("livekit.PushTextRequest", len)?; + if !self.message_id.is_empty() { + struct_ser.serialize_field("messageId", &self.message_id)?; } - if !self.username.is_empty() { - struct_ser.serialize_field("username", &self.username)?; - } - if !self.password.is_empty() { - struct_ser.serialize_field("password", &self.password)?; + if !self.content.is_empty() { + struct_ser.serialize_field("content", &self.content)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for ProxyConfig { +impl<'de> serde::Deserialize<'de> for PushTextRequest { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "url", - "username", - "password", + "message_id", + "messageId", + "content", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Url, - Username, - Password, + MessageId, + Content, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -23001,9 +24171,8 @@ impl<'de> serde::Deserialize<'de> for ProxyConfig { E: serde::de::Error, { match value { - "url" => Ok(GeneratedField::Url), - "username" => Ok(GeneratedField::Username), - "password" => Ok(GeneratedField::Password), + "messageId" | "message_id" => Ok(GeneratedField::MessageId), + "content" => Ok(GeneratedField::Content), _ => Ok(GeneratedField::__SkipField__), } } @@ -23013,52 +24182,44 @@ impl<'de> serde::Deserialize<'de> for ProxyConfig { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = ProxyConfig; + type Value = PushTextRequest; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.ProxyConfig") + formatter.write_str("struct livekit.PushTextRequest") } - 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 url__ = None; - let mut username__ = None; - let mut password__ = None; + let mut message_id__ = None; + let mut content__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Url => { - if url__.is_some() { - return Err(serde::de::Error::duplicate_field("url")); + GeneratedField::MessageId => { + if message_id__.is_some() { + return Err(serde::de::Error::duplicate_field("messageId")); } - url__ = Some(map_.next_value()?); + message_id__ = Some(map_.next_value()?); } - GeneratedField::Username => { - if username__.is_some() { - return Err(serde::de::Error::duplicate_field("username")); - } - username__ = Some(map_.next_value()?); - } - GeneratedField::Password => { - if password__.is_some() { - return Err(serde::de::Error::duplicate_field("password")); + GeneratedField::Content => { + if content__.is_some() { + return Err(serde::de::Error::duplicate_field("content")); } - password__ = Some(map_.next_value()?); + content__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(ProxyConfig { - url: url__.unwrap_or_default(), - username: username__.unwrap_or_default(), - password: password__.unwrap_or_default(), + Ok(PushTextRequest { + message_id: message_id__.unwrap_or_default(), + content: content__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.ProxyConfig", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.PushTextRequest", FIELDS, GeneratedVisitor) } } impl serde::Serialize for RtcpSenderReportState { @@ -25837,6 +26998,12 @@ impl serde::Serialize for RequestResponse { request_response::Request::UpdateVideoTrack(v) => { struct_ser.serialize_field("updateVideoTrack", v)?; } + request_response::Request::PublishDataTrack(v) => { + struct_ser.serialize_field("publishDataTrack", v)?; + } + request_response::Request::UnpublishDataTrack(v) => { + struct_ser.serialize_field("unpublishDataTrack", v)?; + } } } struct_ser.end() @@ -25863,6 +27030,10 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "updateAudioTrack", "update_video_track", "updateVideoTrack", + "publish_data_track", + "publishDataTrack", + "unpublish_data_track", + "unpublishDataTrack", ]; #[allow(clippy::enum_variant_names)] @@ -25876,6 +27047,8 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { UpdateMetadata, UpdateAudioTrack, UpdateVideoTrack, + PublishDataTrack, + UnpublishDataTrack, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -25907,6 +27080,8 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { "updateMetadata" | "update_metadata" => Ok(GeneratedField::UpdateMetadata), "updateAudioTrack" | "update_audio_track" => Ok(GeneratedField::UpdateAudioTrack), "updateVideoTrack" | "update_video_track" => Ok(GeneratedField::UpdateVideoTrack), + "publishDataTrack" | "publish_data_track" => Ok(GeneratedField::PublishDataTrack), + "unpublishDataTrack" | "unpublish_data_track" => Ok(GeneratedField::UnpublishDataTrack), _ => Ok(GeneratedField::__SkipField__), } } @@ -25992,6 +27167,20 @@ impl<'de> serde::Deserialize<'de> for RequestResponse { return Err(serde::de::Error::duplicate_field("updateVideoTrack")); } request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UpdateVideoTrack) +; + } + GeneratedField::PublishDataTrack => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("publishDataTrack")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::PublishDataTrack) +; + } + GeneratedField::UnpublishDataTrack => { + if request__.is_some() { + return Err(serde::de::Error::duplicate_field("unpublishDataTrack")); + } + request__ = map_.next_value::<::std::option::Option<_>>()?.map(request_response::Request::UnpublishDataTrack) ; } GeneratedField::__SkipField__ => { @@ -26024,6 +27213,10 @@ impl serde::Serialize for request_response::Reason { Self::Queued => "QUEUED", Self::UnsupportedType => "UNSUPPORTED_TYPE", Self::UnclassifiedError => "UNCLASSIFIED_ERROR", + Self::InvalidHandle => "INVALID_HANDLE", + Self::InvalidName => "INVALID_NAME", + Self::DuplicateHandle => "DUPLICATE_HANDLE", + Self::DuplicateName => "DUPLICATE_NAME", }; serializer.serialize_str(variant) } @@ -26042,6 +27235,10 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "QUEUED", "UNSUPPORTED_TYPE", "UNCLASSIFIED_ERROR", + "INVALID_HANDLE", + "INVALID_NAME", + "DUPLICATE_HANDLE", + "DUPLICATE_NAME", ]; struct GeneratedVisitor; @@ -26089,6 +27286,10 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "QUEUED" => Ok(request_response::Reason::Queued), "UNSUPPORTED_TYPE" => Ok(request_response::Reason::UnsupportedType), "UNCLASSIFIED_ERROR" => Ok(request_response::Reason::UnclassifiedError), + "INVALID_HANDLE" => Ok(request_response::Reason::InvalidHandle), + "INVALID_NAME" => Ok(request_response::Reason::InvalidName), + "DUPLICATE_HANDLE" => Ok(request_response::Reason::DuplicateHandle), + "DUPLICATE_NAME" => Ok(request_response::Reason::DuplicateName), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -28750,6 +29951,9 @@ impl serde::Serialize for SipCallInfo { if self.provider_info.is_some() { len += 1; } + if !self.sip_call_id.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.SIPCallInfo", len)?; if !self.call_id.is_empty() { struct_ser.serialize_field("callId", &self.call_id)?; @@ -28854,6 +30058,9 @@ impl serde::Serialize for SipCallInfo { if let Some(v) = self.provider_info.as_ref() { struct_ser.serialize_field("providerInfo", v)?; } + if !self.sip_call_id.is_empty() { + struct_ser.serialize_field("sipCallId", &self.sip_call_id)?; + } struct_ser.end() } } @@ -28916,6 +30123,8 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { "callContext", "provider_info", "providerInfo", + "sip_call_id", + "sipCallId", ]; #[allow(clippy::enum_variant_names)] @@ -28947,6 +30156,7 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { PcapFileLink, CallContext, ProviderInfo, + SipCallId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -28996,6 +30206,7 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { "pcapFileLink" | "pcap_file_link" => Ok(GeneratedField::PcapFileLink), "callContext" | "call_context" => Ok(GeneratedField::CallContext), "providerInfo" | "provider_info" => Ok(GeneratedField::ProviderInfo), + "sipCallId" | "sip_call_id" => Ok(GeneratedField::SipCallId), _ => Ok(GeneratedField::__SkipField__), } } @@ -29042,6 +30253,7 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { let mut pcap_file_link__ = None; let mut call_context__ = None; let mut provider_info__ = None; + let mut sip_call_id__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::CallId => { @@ -29220,6 +30432,12 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { } provider_info__ = map_.next_value()?; } + GeneratedField::SipCallId => { + if sip_call_id__.is_some() { + return Err(serde::de::Error::duplicate_field("sipCallId")); + } + sip_call_id__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -29253,6 +30471,7 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { pcap_file_link: pcap_file_link__.unwrap_or_default(), call_context: call_context__.unwrap_or_default(), provider_info: provider_info__, + sip_call_id: sip_call_id__.unwrap_or_default(), }) } } @@ -29847,6 +31066,9 @@ impl serde::Serialize for SipDispatchRuleInfo { if !self.inbound_numbers.is_empty() { len += 1; } + if !self.numbers.is_empty() { + len += 1; + } if !self.name.is_empty() { len += 1; } @@ -29884,6 +31106,9 @@ impl serde::Serialize for SipDispatchRuleInfo { if !self.inbound_numbers.is_empty() { struct_ser.serialize_field("inboundNumbers", &self.inbound_numbers)?; } + if !self.numbers.is_empty() { + struct_ser.serialize_field("numbers", &self.numbers)?; + } if !self.name.is_empty() { struct_ser.serialize_field("name", &self.name)?; } @@ -29926,6 +31151,7 @@ impl<'de> serde::Deserialize<'de> for SipDispatchRuleInfo { "hidePhoneNumber", "inbound_numbers", "inboundNumbers", + "numbers", "name", "metadata", "attributes", @@ -29946,6 +31172,7 @@ impl<'de> serde::Deserialize<'de> for SipDispatchRuleInfo { TrunkIds, HidePhoneNumber, InboundNumbers, + Numbers, Name, Metadata, Attributes, @@ -29980,6 +31207,7 @@ impl<'de> serde::Deserialize<'de> for SipDispatchRuleInfo { "trunkIds" | "trunk_ids" => Ok(GeneratedField::TrunkIds), "hidePhoneNumber" | "hide_phone_number" => Ok(GeneratedField::HidePhoneNumber), "inboundNumbers" | "inbound_numbers" => Ok(GeneratedField::InboundNumbers), + "numbers" => Ok(GeneratedField::Numbers), "name" => Ok(GeneratedField::Name), "metadata" => Ok(GeneratedField::Metadata), "attributes" => Ok(GeneratedField::Attributes), @@ -30011,6 +31239,7 @@ impl<'de> serde::Deserialize<'de> for SipDispatchRuleInfo { let mut trunk_ids__ = None; let mut hide_phone_number__ = None; let mut inbound_numbers__ = None; + let mut numbers__ = None; let mut name__ = None; let mut metadata__ = None; let mut attributes__ = None; @@ -30050,6 +31279,12 @@ impl<'de> serde::Deserialize<'de> for SipDispatchRuleInfo { } inbound_numbers__ = Some(map_.next_value()?); } + GeneratedField::Numbers => { + if numbers__.is_some() { + return Err(serde::de::Error::duplicate_field("numbers")); + } + numbers__ = Some(map_.next_value()?); + } GeneratedField::Name => { if name__.is_some() { return Err(serde::de::Error::duplicate_field("name")); @@ -30105,6 +31340,7 @@ impl<'de> serde::Deserialize<'de> for SipDispatchRuleInfo { trunk_ids: trunk_ids__.unwrap_or_default(), hide_phone_number: hide_phone_number__.unwrap_or_default(), inbound_numbers: inbound_numbers__.unwrap_or_default(), + numbers: numbers__.unwrap_or_default(), name: name__.unwrap_or_default(), metadata: metadata__.unwrap_or_default(), attributes: attributes__.unwrap_or_default(), @@ -34666,6 +35902,9 @@ impl serde::Serialize for ServerMessage { server_message::Message::Pong(v) => { struct_ser.serialize_field("pong", v)?; } + server_message::Message::TextRequest(v) => { + struct_ser.serialize_field("textRequest", v)?; + } } } struct_ser.end() @@ -34683,6 +35922,8 @@ impl<'de> serde::Deserialize<'de> for ServerMessage { "assignment", "termination", "pong", + "text_request", + "textRequest", ]; #[allow(clippy::enum_variant_names)] @@ -34692,6 +35933,7 @@ impl<'de> serde::Deserialize<'de> for ServerMessage { Assignment, Termination, Pong, + TextRequest, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -34719,6 +35961,7 @@ impl<'de> serde::Deserialize<'de> for ServerMessage { "assignment" => Ok(GeneratedField::Assignment), "termination" => Ok(GeneratedField::Termination), "pong" => Ok(GeneratedField::Pong), + "textRequest" | "text_request" => Ok(GeneratedField::TextRequest), _ => Ok(GeneratedField::__SkipField__), } } @@ -34774,6 +36017,13 @@ impl<'de> serde::Deserialize<'de> for ServerMessage { return Err(serde::de::Error::duplicate_field("pong")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(server_message::Message::Pong) +; + } + GeneratedField::TextRequest => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("textRequest")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(server_message::Message::TextRequest) ; } GeneratedField::__SkipField__ => { @@ -35007,6 +36257,15 @@ impl serde::Serialize for SignalRequest { signal_request::Message::UpdateVideoTrack(v) => { struct_ser.serialize_field("updateVideoTrack", v)?; } + signal_request::Message::PublishDataTrackRequest(v) => { + struct_ser.serialize_field("publishDataTrackRequest", v)?; + } + signal_request::Message::UnpublishDataTrackRequest(v) => { + struct_ser.serialize_field("unpublishDataTrackRequest", v)?; + } + signal_request::Message::UpdateDataSubscription(v) => { + struct_ser.serialize_field("updateDataSubscription", v)?; + } } } struct_ser.end() @@ -35045,6 +36304,12 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "updateAudioTrack", "update_video_track", "updateVideoTrack", + "publish_data_track_request", + "publishDataTrackRequest", + "unpublish_data_track_request", + "unpublishDataTrackRequest", + "update_data_subscription", + "updateDataSubscription", ]; #[allow(clippy::enum_variant_names)] @@ -35066,6 +36331,9 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { PingReq, UpdateAudioTrack, UpdateVideoTrack, + PublishDataTrackRequest, + UnpublishDataTrackRequest, + UpdateDataSubscription, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -35105,6 +36373,9 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "pingReq" | "ping_req" => Ok(GeneratedField::PingReq), "updateAudioTrack" | "update_audio_track" => Ok(GeneratedField::UpdateAudioTrack), "updateVideoTrack" | "update_video_track" => Ok(GeneratedField::UpdateVideoTrack), + "publishDataTrackRequest" | "publish_data_track_request" => Ok(GeneratedField::PublishDataTrackRequest), + "unpublishDataTrackRequest" | "unpublish_data_track_request" => Ok(GeneratedField::UnpublishDataTrackRequest), + "updateDataSubscription" | "update_data_subscription" => Ok(GeneratedField::UpdateDataSubscription), _ => Ok(GeneratedField::__SkipField__), } } @@ -35243,6 +36514,27 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { return Err(serde::de::Error::duplicate_field("updateVideoTrack")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UpdateVideoTrack) +; + } + GeneratedField::PublishDataTrackRequest => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("publishDataTrackRequest")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::PublishDataTrackRequest) +; + } + GeneratedField::UnpublishDataTrackRequest => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("unpublishDataTrackRequest")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UnpublishDataTrackRequest) +; + } + GeneratedField::UpdateDataSubscription => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("updateDataSubscription")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UpdateDataSubscription) ; } GeneratedField::__SkipField__ => { @@ -35349,6 +36641,15 @@ impl serde::Serialize for SignalResponse { signal_response::Message::SubscribedAudioCodecUpdate(v) => { struct_ser.serialize_field("subscribedAudioCodecUpdate", v)?; } + signal_response::Message::PublishDataTrackResponse(v) => { + struct_ser.serialize_field("publishDataTrackResponse", v)?; + } + signal_response::Message::UnpublishDataTrackResponse(v) => { + struct_ser.serialize_field("unpublishDataTrackResponse", v)?; + } + signal_response::Message::DataTrackSubscriberHandles(v) => { + struct_ser.serialize_field("dataTrackSubscriberHandles", v)?; + } } } struct_ser.end() @@ -35402,6 +36703,12 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "mediaSectionsRequirement", "subscribed_audio_codec_update", "subscribedAudioCodecUpdate", + "publish_data_track_response", + "publishDataTrackResponse", + "unpublish_data_track_response", + "unpublishDataTrackResponse", + "data_track_subscriber_handles", + "dataTrackSubscriberHandles", ]; #[allow(clippy::enum_variant_names)] @@ -35431,6 +36738,9 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { RoomMoved, MediaSectionsRequirement, SubscribedAudioCodecUpdate, + PublishDataTrackResponse, + UnpublishDataTrackResponse, + DataTrackSubscriberHandles, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -35478,6 +36788,9 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "roomMoved" | "room_moved" => Ok(GeneratedField::RoomMoved), "mediaSectionsRequirement" | "media_sections_requirement" => Ok(GeneratedField::MediaSectionsRequirement), "subscribedAudioCodecUpdate" | "subscribed_audio_codec_update" => Ok(GeneratedField::SubscribedAudioCodecUpdate), + "publishDataTrackResponse" | "publish_data_track_response" => Ok(GeneratedField::PublishDataTrackResponse), + "unpublishDataTrackResponse" | "unpublish_data_track_response" => Ok(GeneratedField::UnpublishDataTrackResponse), + "dataTrackSubscriberHandles" | "data_track_subscriber_handles" => Ok(GeneratedField::DataTrackSubscriberHandles), _ => Ok(GeneratedField::__SkipField__), } } @@ -35671,6 +36984,27 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { return Err(serde::de::Error::duplicate_field("subscribedAudioCodecUpdate")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::SubscribedAudioCodecUpdate) +; + } + GeneratedField::PublishDataTrackResponse => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("publishDataTrackResponse")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::PublishDataTrackResponse) +; + } + GeneratedField::UnpublishDataTrackResponse => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("unpublishDataTrackResponse")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::UnpublishDataTrackResponse) +; + } + GeneratedField::DataTrackSubscriberHandles => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("dataTrackSubscriberHandles")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::DataTrackSubscriberHandles) ; } GeneratedField::__SkipField__ => { @@ -38806,6 +40140,9 @@ impl serde::Serialize for SyncState { if !self.datachannel_receive_states.is_empty() { len += 1; } + if !self.publish_data_tracks.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.SyncState", len)?; if let Some(v) = self.answer.as_ref() { struct_ser.serialize_field("answer", v)?; @@ -38828,6 +40165,9 @@ impl serde::Serialize for SyncState { if !self.datachannel_receive_states.is_empty() { struct_ser.serialize_field("datachannelReceiveStates", &self.datachannel_receive_states)?; } + if !self.publish_data_tracks.is_empty() { + struct_ser.serialize_field("publishDataTracks", &self.publish_data_tracks)?; + } struct_ser.end() } } @@ -38849,6 +40189,8 @@ impl<'de> serde::Deserialize<'de> for SyncState { "trackSidsDisabled", "datachannel_receive_states", "datachannelReceiveStates", + "publish_data_tracks", + "publishDataTracks", ]; #[allow(clippy::enum_variant_names)] @@ -38860,6 +40202,7 @@ impl<'de> serde::Deserialize<'de> for SyncState { Offer, TrackSidsDisabled, DatachannelReceiveStates, + PublishDataTracks, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -38889,6 +40232,7 @@ impl<'de> serde::Deserialize<'de> for SyncState { "offer" => Ok(GeneratedField::Offer), "trackSidsDisabled" | "track_sids_disabled" => Ok(GeneratedField::TrackSidsDisabled), "datachannelReceiveStates" | "datachannel_receive_states" => Ok(GeneratedField::DatachannelReceiveStates), + "publishDataTracks" | "publish_data_tracks" => Ok(GeneratedField::PublishDataTracks), _ => Ok(GeneratedField::__SkipField__), } } @@ -38915,6 +40259,7 @@ impl<'de> serde::Deserialize<'de> for SyncState { let mut offer__ = None; let mut track_sids_disabled__ = None; let mut datachannel_receive_states__ = None; + let mut publish_data_tracks__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Answer => { @@ -38959,6 +40304,12 @@ impl<'de> serde::Deserialize<'de> for SyncState { } datachannel_receive_states__ = Some(map_.next_value()?); } + GeneratedField::PublishDataTracks => { + if publish_data_tracks__.is_some() { + return Err(serde::de::Error::duplicate_field("publishDataTracks")); + } + publish_data_tracks__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -38972,12 +40323,336 @@ impl<'de> serde::Deserialize<'de> for SyncState { offer: offer__, track_sids_disabled: track_sids_disabled__.unwrap_or_default(), datachannel_receive_states: datachannel_receive_states__.unwrap_or_default(), + publish_data_tracks: publish_data_tracks__.unwrap_or_default(), }) } } deserializer.deserialize_struct("livekit.SyncState", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for TextMessageRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.message_id.is_empty() { + len += 1; + } + if !self.session_id.is_empty() { + len += 1; + } + if !self.agent_name.is_empty() { + len += 1; + } + if !self.metadata.is_empty() { + len += 1; + } + if !self.session_data.is_empty() { + len += 1; + } + if !self.text.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.TextMessageRequest", len)?; + if !self.message_id.is_empty() { + struct_ser.serialize_field("messageId", &self.message_id)?; + } + if !self.session_id.is_empty() { + struct_ser.serialize_field("sessionId", &self.session_id)?; + } + if !self.agent_name.is_empty() { + struct_ser.serialize_field("agentName", &self.agent_name)?; + } + if !self.metadata.is_empty() { + struct_ser.serialize_field("metadata", &self.metadata)?; + } + if !self.session_data.is_empty() { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("sessionData", pbjson::private::base64::encode(&self.session_data).as_str())?; + } + if !self.text.is_empty() { + struct_ser.serialize_field("text", &self.text)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for TextMessageRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "message_id", + "messageId", + "session_id", + "sessionId", + "agent_name", + "agentName", + "metadata", + "session_data", + "sessionData", + "text", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + MessageId, + SessionId, + AgentName, + Metadata, + SessionData, + Text, + __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 { + "messageId" | "message_id" => Ok(GeneratedField::MessageId), + "sessionId" | "session_id" => Ok(GeneratedField::SessionId), + "agentName" | "agent_name" => Ok(GeneratedField::AgentName), + "metadata" => Ok(GeneratedField::Metadata), + "sessionData" | "session_data" => Ok(GeneratedField::SessionData), + "text" => Ok(GeneratedField::Text), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TextMessageRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.TextMessageRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut message_id__ = None; + let mut session_id__ = None; + let mut agent_name__ = None; + let mut metadata__ = None; + let mut session_data__ = None; + let mut text__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::MessageId => { + if message_id__.is_some() { + return Err(serde::de::Error::duplicate_field("messageId")); + } + message_id__ = Some(map_.next_value()?); + } + GeneratedField::SessionId => { + if session_id__.is_some() { + return Err(serde::de::Error::duplicate_field("sessionId")); + } + session_id__ = Some(map_.next_value()?); + } + GeneratedField::AgentName => { + if agent_name__.is_some() { + return Err(serde::de::Error::duplicate_field("agentName")); + } + agent_name__ = Some(map_.next_value()?); + } + GeneratedField::Metadata => { + if metadata__.is_some() { + return Err(serde::de::Error::duplicate_field("metadata")); + } + metadata__ = Some(map_.next_value()?); + } + GeneratedField::SessionData => { + if session_data__.is_some() { + return Err(serde::de::Error::duplicate_field("sessionData")); + } + session_data__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::Text => { + if text__.is_some() { + return Err(serde::de::Error::duplicate_field("text")); + } + text__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(TextMessageRequest { + message_id: message_id__.unwrap_or_default(), + session_id: session_id__.unwrap_or_default(), + agent_name: agent_name__.unwrap_or_default(), + metadata: metadata__.unwrap_or_default(), + session_data: session_data__.unwrap_or_default(), + text: text__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.TextMessageRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for TextMessageResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.message_id.is_empty() { + len += 1; + } + if !self.session_data.is_empty() { + len += 1; + } + if !self.error.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.TextMessageResponse", len)?; + if !self.message_id.is_empty() { + struct_ser.serialize_field("messageId", &self.message_id)?; + } + if !self.session_data.is_empty() { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("sessionData", pbjson::private::base64::encode(&self.session_data).as_str())?; + } + if !self.error.is_empty() { + struct_ser.serialize_field("error", &self.error)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for TextMessageResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "message_id", + "messageId", + "session_data", + "sessionData", + "error", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + MessageId, + SessionData, + Error, + __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 { + "messageId" | "message_id" => Ok(GeneratedField::MessageId), + "sessionData" | "session_data" => Ok(GeneratedField::SessionData), + "error" => Ok(GeneratedField::Error), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TextMessageResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.TextMessageResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut message_id__ = None; + let mut session_data__ = None; + let mut error__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::MessageId => { + if message_id__.is_some() { + return Err(serde::de::Error::duplicate_field("messageId")); + } + message_id__ = Some(map_.next_value()?); + } + GeneratedField::SessionData => { + if session_data__.is_some() { + return Err(serde::de::Error::duplicate_field("sessionData")); + } + session_data__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::Error => { + if error__.is_some() { + return Err(serde::de::Error::duplicate_field("error")); + } + error__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(TextMessageResponse { + message_id: message_id__.unwrap_or_default(), + session_data: session_data__.unwrap_or_default(), + error: error__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.TextMessageResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for TimeSeriesMetric { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -40614,41 +42289,483 @@ impl<'de> serde::Deserialize<'de> for TrackSource { } } } - deserializer.deserialize_any(GeneratedVisitor) - } -} -impl serde::Serialize for TrackSubscribed { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut len = 0; - if !self.track_sid.is_empty() { - len += 1; + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for TrackSubscribed { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.track_sid.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.TrackSubscribed", len)?; + if !self.track_sid.is_empty() { + struct_ser.serialize_field("trackSid", &self.track_sid)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for TrackSubscribed { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "track_sid", + "trackSid", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + TrackSid, + __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 { + "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TrackSubscribed; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.TrackSubscribed") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut track_sid__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::TrackSid => { + if track_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSid")); + } + track_sid__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(TrackSubscribed { + track_sid: track_sid__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.TrackSubscribed", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for TrackType { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Audio => "AUDIO", + Self::Video => "VIDEO", + Self::Data => "DATA", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for TrackType { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "AUDIO", + "VIDEO", + "DATA", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TrackType; + + 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 { + "AUDIO" => Ok(TrackType::Audio), + "VIDEO" => Ok(TrackType::Video), + "DATA" => Ok(TrackType::Data), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for TrackUnpublishedResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.track_sid.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.TrackUnpublishedResponse", len)?; + if !self.track_sid.is_empty() { + struct_ser.serialize_field("trackSid", &self.track_sid)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for TrackUnpublishedResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "track_sid", + "trackSid", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + TrackSid, + __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 { + "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TrackUnpublishedResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.TrackUnpublishedResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut track_sid__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::TrackSid => { + if track_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSid")); + } + track_sid__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(TrackUnpublishedResponse { + track_sid: track_sid__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.TrackUnpublishedResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Transcription { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.transcribed_participant_identity.is_empty() { + len += 1; + } + if !self.track_id.is_empty() { + len += 1; + } + if !self.segments.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.Transcription", len)?; + if !self.transcribed_participant_identity.is_empty() { + struct_ser.serialize_field("transcribedParticipantIdentity", &self.transcribed_participant_identity)?; + } + if !self.track_id.is_empty() { + struct_ser.serialize_field("trackId", &self.track_id)?; + } + if !self.segments.is_empty() { + struct_ser.serialize_field("segments", &self.segments)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Transcription { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "transcribed_participant_identity", + "transcribedParticipantIdentity", + "track_id", + "trackId", + "segments", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + TranscribedParticipantIdentity, + TrackId, + Segments, + __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 { + "transcribedParticipantIdentity" | "transcribed_participant_identity" => Ok(GeneratedField::TranscribedParticipantIdentity), + "trackId" | "track_id" => Ok(GeneratedField::TrackId), + "segments" => Ok(GeneratedField::Segments), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Transcription; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.Transcription") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut transcribed_participant_identity__ = None; + let mut track_id__ = None; + let mut segments__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::TranscribedParticipantIdentity => { + if transcribed_participant_identity__.is_some() { + return Err(serde::de::Error::duplicate_field("transcribedParticipantIdentity")); + } + transcribed_participant_identity__ = Some(map_.next_value()?); + } + GeneratedField::TrackId => { + if track_id__.is_some() { + return Err(serde::de::Error::duplicate_field("trackId")); + } + track_id__ = Some(map_.next_value()?); + } + GeneratedField::Segments => { + if segments__.is_some() { + return Err(serde::de::Error::duplicate_field("segments")); + } + segments__ = Some(map_.next_value()?); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(Transcription { + transcribed_participant_identity: transcribed_participant_identity__.unwrap_or_default(), + track_id: track_id__.unwrap_or_default(), + segments: segments__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.Transcription", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for TranscriptionSegment { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.id.is_empty() { + len += 1; + } + if !self.text.is_empty() { + len += 1; + } + if self.start_time != 0 { + len += 1; + } + if self.end_time != 0 { + len += 1; + } + if self.r#final { + len += 1; + } + if !self.language.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.TranscriptionSegment", len)?; + if !self.id.is_empty() { + struct_ser.serialize_field("id", &self.id)?; + } + if !self.text.is_empty() { + struct_ser.serialize_field("text", &self.text)?; + } + if self.start_time != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("startTime", ToString::to_string(&self.start_time).as_str())?; + } + if self.end_time != 0 { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("endTime", ToString::to_string(&self.end_time).as_str())?; + } + if self.r#final { + struct_ser.serialize_field("final", &self.r#final)?; } - let mut struct_ser = serializer.serialize_struct("livekit.TrackSubscribed", len)?; - if !self.track_sid.is_empty() { - struct_ser.serialize_field("trackSid", &self.track_sid)?; + if !self.language.is_empty() { + struct_ser.serialize_field("language", &self.language)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for TrackSubscribed { +impl<'de> serde::Deserialize<'de> for TranscriptionSegment { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "track_sid", - "trackSid", + "id", + "text", + "start_time", + "startTime", + "end_time", + "endTime", + "final", + "language", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - TrackSid, + Id, + Text, + StartTime, + EndTime, + Final, + Language, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -40671,7 +42788,12 @@ impl<'de> serde::Deserialize<'de> for TrackSubscribed { E: serde::de::Error, { match value { - "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), + "id" => Ok(GeneratedField::Id), + "text" => Ok(GeneratedField::Text), + "startTime" | "start_time" => Ok(GeneratedField::StartTime), + "endTime" | "end_time" => Ok(GeneratedField::EndTime), + "final" => Ok(GeneratedField::Final), + "language" => Ok(GeneratedField::Language), _ => Ok(GeneratedField::__SkipField__), } } @@ -40681,113 +42803,270 @@ impl<'de> serde::Deserialize<'de> for TrackSubscribed { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = TrackSubscribed; + type Value = TranscriptionSegment; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.TrackSubscribed") + formatter.write_str("struct livekit.TranscriptionSegment") } - 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 track_sid__ = None; + let mut id__ = None; + let mut text__ = None; + let mut start_time__ = None; + let mut end_time__ = None; + let mut r#final__ = None; + let mut language__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::TrackSid => { - if track_sid__.is_some() { - return Err(serde::de::Error::duplicate_field("trackSid")); + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); } - track_sid__ = Some(map_.next_value()?); + id__ = Some(map_.next_value()?); + } + GeneratedField::Text => { + if text__.is_some() { + return Err(serde::de::Error::duplicate_field("text")); + } + text__ = Some(map_.next_value()?); + } + GeneratedField::StartTime => { + if start_time__.is_some() { + return Err(serde::de::Error::duplicate_field("startTime")); + } + start_time__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::EndTime => { + if end_time__.is_some() { + return Err(serde::de::Error::duplicate_field("endTime")); + } + end_time__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Final => { + if r#final__.is_some() { + return Err(serde::de::Error::duplicate_field("final")); + } + r#final__ = Some(map_.next_value()?); + } + GeneratedField::Language => { + if language__.is_some() { + return Err(serde::de::Error::duplicate_field("language")); + } + language__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(TrackSubscribed { - track_sid: track_sid__.unwrap_or_default(), + Ok(TranscriptionSegment { + id: id__.unwrap_or_default(), + text: text__.unwrap_or_default(), + start_time: start_time__.unwrap_or_default(), + end_time: end_time__.unwrap_or_default(), + r#final: r#final__.unwrap_or_default(), + language: language__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.TrackSubscribed", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.TranscriptionSegment", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for TrackType { +impl serde::Serialize for TransferSipParticipantRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { - let variant = match self { - Self::Audio => "AUDIO", - Self::Video => "VIDEO", - Self::Data => "DATA", - }; - serializer.serialize_str(variant) + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.participant_identity.is_empty() { + len += 1; + } + if !self.room_name.is_empty() { + len += 1; + } + if !self.transfer_to.is_empty() { + len += 1; + } + if self.play_dialtone { + len += 1; + } + if !self.headers.is_empty() { + len += 1; + } + if self.ringing_timeout.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.TransferSIPParticipantRequest", len)?; + if !self.participant_identity.is_empty() { + struct_ser.serialize_field("participantIdentity", &self.participant_identity)?; + } + if !self.room_name.is_empty() { + struct_ser.serialize_field("roomName", &self.room_name)?; + } + if !self.transfer_to.is_empty() { + struct_ser.serialize_field("transferTo", &self.transfer_to)?; + } + if self.play_dialtone { + struct_ser.serialize_field("playDialtone", &self.play_dialtone)?; + } + if !self.headers.is_empty() { + struct_ser.serialize_field("headers", &self.headers)?; + } + if let Some(v) = self.ringing_timeout.as_ref() { + struct_ser.serialize_field("ringingTimeout", v)?; + } + struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for TrackType { +impl<'de> serde::Deserialize<'de> for TransferSipParticipantRequest { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "AUDIO", - "VIDEO", - "DATA", + "participant_identity", + "participantIdentity", + "room_name", + "roomName", + "transfer_to", + "transferTo", + "play_dialtone", + "playDialtone", + "headers", + "ringing_timeout", + "ringingTimeout", ]; - struct GeneratedVisitor; + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + ParticipantIdentity, + RoomName, + TransferTo, + PlayDialtone, + Headers, + RingingTimeout, + __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 = TrackType; + 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) - } + 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) - }) + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), + "roomName" | "room_name" => Ok(GeneratedField::RoomName), + "transferTo" | "transfer_to" => Ok(GeneratedField::TransferTo), + "playDialtone" | "play_dialtone" => Ok(GeneratedField::PlayDialtone), + "headers" => Ok(GeneratedField::Headers), + "ringingTimeout" | "ringing_timeout" => Ok(GeneratedField::RingingTimeout), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TransferSipParticipantRequest; - 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 expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.TransferSIPParticipantRequest") } - fn visit_str(self, value: &str) -> std::result::Result - where - E: serde::de::Error, + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, { - match value { - "AUDIO" => Ok(TrackType::Audio), - "VIDEO" => Ok(TrackType::Video), - "DATA" => Ok(TrackType::Data), - _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + let mut participant_identity__ = None; + let mut room_name__ = None; + let mut transfer_to__ = None; + let mut play_dialtone__ = None; + let mut headers__ = None; + let mut ringing_timeout__ = 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::RoomName => { + if room_name__.is_some() { + return Err(serde::de::Error::duplicate_field("roomName")); + } + room_name__ = Some(map_.next_value()?); + } + GeneratedField::TransferTo => { + if transfer_to__.is_some() { + return Err(serde::de::Error::duplicate_field("transferTo")); + } + transfer_to__ = Some(map_.next_value()?); + } + GeneratedField::PlayDialtone => { + if play_dialtone__.is_some() { + return Err(serde::de::Error::duplicate_field("playDialtone")); + } + play_dialtone__ = Some(map_.next_value()?); + } + GeneratedField::Headers => { + if headers__.is_some() { + return Err(serde::de::Error::duplicate_field("headers")); + } + headers__ = Some( + map_.next_value::>()? + ); + } + GeneratedField::RingingTimeout => { + if ringing_timeout__.is_some() { + return Err(serde::de::Error::duplicate_field("ringingTimeout")); + } + ringing_timeout__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } } + Ok(TransferSipParticipantRequest { + participant_identity: participant_identity__.unwrap_or_default(), + room_name: room_name__.unwrap_or_default(), + transfer_to: transfer_to__.unwrap_or_default(), + play_dialtone: play_dialtone__.unwrap_or_default(), + headers: headers__.unwrap_or_default(), + ringing_timeout: ringing_timeout__, + }) } } - deserializer.deserialize_any(GeneratedVisitor) + deserializer.deserialize_struct("livekit.TransferSIPParticipantRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for TrackUnpublishedResponse { +impl serde::Serialize for TrickleRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -40795,30 +43074,47 @@ impl serde::Serialize for TrackUnpublishedResponse { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.track_sid.is_empty() { + if !self.candidate_init.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.TrackUnpublishedResponse", len)?; - if !self.track_sid.is_empty() { - struct_ser.serialize_field("trackSid", &self.track_sid)?; + if self.target != 0 { + len += 1; + } + if self.r#final { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.TrickleRequest", len)?; + if !self.candidate_init.is_empty() { + struct_ser.serialize_field("candidateInit", &self.candidate_init)?; + } + if self.target != 0 { + let v = SignalTarget::try_from(self.target) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.target)))?; + struct_ser.serialize_field("target", &v)?; + } + if self.r#final { + struct_ser.serialize_field("final", &self.r#final)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for TrackUnpublishedResponse { +impl<'de> serde::Deserialize<'de> for TrickleRequest { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "track_sid", - "trackSid", + "candidateInit", + "target", + "final", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - TrackSid, + CandidateInit, + Target, + Final, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -40841,7 +43137,9 @@ impl<'de> serde::Deserialize<'de> for TrackUnpublishedResponse { E: serde::de::Error, { match value { - "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), + "candidateInit" => Ok(GeneratedField::CandidateInit), + "target" => Ok(GeneratedField::Target), + "final" => Ok(GeneratedField::Final), _ => Ok(GeneratedField::__SkipField__), } } @@ -40851,39 +43149,55 @@ impl<'de> serde::Deserialize<'de> for TrackUnpublishedResponse { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = TrackUnpublishedResponse; + type Value = TrickleRequest; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.TrackUnpublishedResponse") + formatter.write_str("struct livekit.TrickleRequest") } - 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 track_sid__ = None; + let mut candidate_init__ = None; + let mut target__ = None; + let mut r#final__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::TrackSid => { - if track_sid__.is_some() { - return Err(serde::de::Error::duplicate_field("trackSid")); + GeneratedField::CandidateInit => { + if candidate_init__.is_some() { + return Err(serde::de::Error::duplicate_field("candidateInit")); } - track_sid__ = Some(map_.next_value()?); + candidate_init__ = Some(map_.next_value()?); + } + GeneratedField::Target => { + if target__.is_some() { + return Err(serde::de::Error::duplicate_field("target")); + } + target__ = Some(map_.next_value::()? as i32); + } + GeneratedField::Final => { + if r#final__.is_some() { + return Err(serde::de::Error::duplicate_field("final")); + } + r#final__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(TrackUnpublishedResponse { - track_sid: track_sid__.unwrap_or_default(), + Ok(TrickleRequest { + candidate_init: candidate_init__.unwrap_or_default(), + target: target__.unwrap_or_default(), + r#final: r#final__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.TrackUnpublishedResponse", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.TrickleRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for Transcription { +impl serde::Serialize for UnpublishDataTrackRequest { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -40891,47 +43205,30 @@ impl serde::Serialize for Transcription { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.transcribed_participant_identity.is_empty() { + if self.pub_handle != 0 { len += 1; } - if !self.track_id.is_empty() { - len += 1; - } - if !self.segments.is_empty() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.Transcription", len)?; - if !self.transcribed_participant_identity.is_empty() { - struct_ser.serialize_field("transcribedParticipantIdentity", &self.transcribed_participant_identity)?; - } - if !self.track_id.is_empty() { - struct_ser.serialize_field("trackId", &self.track_id)?; - } - if !self.segments.is_empty() { - struct_ser.serialize_field("segments", &self.segments)?; + let mut struct_ser = serializer.serialize_struct("livekit.UnpublishDataTrackRequest", len)?; + if self.pub_handle != 0 { + struct_ser.serialize_field("pubHandle", &self.pub_handle)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for Transcription { +impl<'de> serde::Deserialize<'de> for UnpublishDataTrackRequest { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "transcribed_participant_identity", - "transcribedParticipantIdentity", - "track_id", - "trackId", - "segments", + "pub_handle", + "pubHandle", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - TranscribedParticipantIdentity, - TrackId, - Segments, + PubHandle, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -40954,9 +43251,7 @@ impl<'de> serde::Deserialize<'de> for Transcription { E: serde::de::Error, { match value { - "transcribedParticipantIdentity" | "transcribed_participant_identity" => Ok(GeneratedField::TranscribedParticipantIdentity), - "trackId" | "track_id" => Ok(GeneratedField::TrackId), - "segments" => Ok(GeneratedField::Segments), + "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), _ => Ok(GeneratedField::__SkipField__), } } @@ -40966,55 +43261,41 @@ impl<'de> serde::Deserialize<'de> for Transcription { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = Transcription; + type Value = UnpublishDataTrackRequest; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.Transcription") + formatter.write_str("struct livekit.UnpublishDataTrackRequest") } - 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 transcribed_participant_identity__ = None; - let mut track_id__ = None; - let mut segments__ = None; + let mut pub_handle__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::TranscribedParticipantIdentity => { - if transcribed_participant_identity__.is_some() { - return Err(serde::de::Error::duplicate_field("transcribedParticipantIdentity")); - } - transcribed_participant_identity__ = Some(map_.next_value()?); - } - GeneratedField::TrackId => { - if track_id__.is_some() { - return Err(serde::de::Error::duplicate_field("trackId")); - } - track_id__ = Some(map_.next_value()?); - } - GeneratedField::Segments => { - if segments__.is_some() { - return Err(serde::de::Error::duplicate_field("segments")); + GeneratedField::PubHandle => { + if pub_handle__.is_some() { + return Err(serde::de::Error::duplicate_field("pubHandle")); } - segments__ = Some(map_.next_value()?); + pub_handle__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(Transcription { - transcribed_participant_identity: transcribed_participant_identity__.unwrap_or_default(), - track_id: track_id__.unwrap_or_default(), - segments: segments__.unwrap_or_default(), + Ok(UnpublishDataTrackRequest { + pub_handle: pub_handle__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.Transcription", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.UnpublishDataTrackRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for TranscriptionSegment { +impl serde::Serialize for UnpublishDataTrackResponse { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -41022,75 +43303,29 @@ impl serde::Serialize for TranscriptionSegment { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.id.is_empty() { - len += 1; - } - if !self.text.is_empty() { - len += 1; - } - if self.start_time != 0 { + if self.info.is_some() { len += 1; } - if self.end_time != 0 { - len += 1; - } - if self.r#final { - len += 1; - } - if !self.language.is_empty() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.TranscriptionSegment", len)?; - if !self.id.is_empty() { - struct_ser.serialize_field("id", &self.id)?; - } - if !self.text.is_empty() { - struct_ser.serialize_field("text", &self.text)?; - } - if self.start_time != 0 { - #[allow(clippy::needless_borrow)] - #[allow(clippy::needless_borrows_for_generic_args)] - struct_ser.serialize_field("startTime", ToString::to_string(&self.start_time).as_str())?; - } - if self.end_time != 0 { - #[allow(clippy::needless_borrow)] - #[allow(clippy::needless_borrows_for_generic_args)] - struct_ser.serialize_field("endTime", ToString::to_string(&self.end_time).as_str())?; - } - if self.r#final { - struct_ser.serialize_field("final", &self.r#final)?; - } - if !self.language.is_empty() { - struct_ser.serialize_field("language", &self.language)?; + let mut struct_ser = serializer.serialize_struct("livekit.UnpublishDataTrackResponse", len)?; + if let Some(v) = self.info.as_ref() { + struct_ser.serialize_field("info", v)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for TranscriptionSegment { +impl<'de> serde::Deserialize<'de> for UnpublishDataTrackResponse { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "id", - "text", - "start_time", - "startTime", - "end_time", - "endTime", - "final", - "language", + "info", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - Id, - Text, - StartTime, - EndTime, - Final, - Language, + Info, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -41113,12 +43348,7 @@ impl<'de> serde::Deserialize<'de> for TranscriptionSegment { E: serde::de::Error, { match value { - "id" => Ok(GeneratedField::Id), - "text" => Ok(GeneratedField::Text), - "startTime" | "start_time" => Ok(GeneratedField::StartTime), - "endTime" | "end_time" => Ok(GeneratedField::EndTime), - "final" => Ok(GeneratedField::Final), - "language" => Ok(GeneratedField::Language), + "info" => Ok(GeneratedField::Info), _ => Ok(GeneratedField::__SkipField__), } } @@ -41128,83 +43358,39 @@ impl<'de> serde::Deserialize<'de> for TranscriptionSegment { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = TranscriptionSegment; + type Value = UnpublishDataTrackResponse; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.TranscriptionSegment") + formatter.write_str("struct livekit.UnpublishDataTrackResponse") } - 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 id__ = None; - let mut text__ = None; - let mut start_time__ = None; - let mut end_time__ = None; - let mut r#final__ = None; - let mut language__ = None; + let mut info__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Id => { - if id__.is_some() { - return Err(serde::de::Error::duplicate_field("id")); - } - id__ = Some(map_.next_value()?); - } - GeneratedField::Text => { - if text__.is_some() { - return Err(serde::de::Error::duplicate_field("text")); - } - text__ = Some(map_.next_value()?); - } - GeneratedField::StartTime => { - if start_time__.is_some() { - return Err(serde::de::Error::duplicate_field("startTime")); - } - start_time__ = - Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) - ; - } - GeneratedField::EndTime => { - if end_time__.is_some() { - return Err(serde::de::Error::duplicate_field("endTime")); - } - end_time__ = - Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) - ; - } - GeneratedField::Final => { - if r#final__.is_some() { - return Err(serde::de::Error::duplicate_field("final")); - } - r#final__ = Some(map_.next_value()?); - } - GeneratedField::Language => { - if language__.is_some() { - return Err(serde::de::Error::duplicate_field("language")); + GeneratedField::Info => { + if info__.is_some() { + return Err(serde::de::Error::duplicate_field("info")); } - language__ = Some(map_.next_value()?); + info__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(TranscriptionSegment { - id: id__.unwrap_or_default(), - text: text__.unwrap_or_default(), - start_time: start_time__.unwrap_or_default(), - end_time: end_time__.unwrap_or_default(), - r#final: r#final__.unwrap_or_default(), - language: language__.unwrap_or_default(), + Ok(UnpublishDataTrackResponse { + info: info__, }) } } - deserializer.deserialize_struct("livekit.TranscriptionSegment", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.UnpublishDataTrackResponse", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for TransferSipParticipantRequest { +impl serde::Serialize for UpdateDataSubscription { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -41212,74 +43398,29 @@ impl serde::Serialize for TransferSipParticipantRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.participant_identity.is_empty() { - len += 1; - } - if !self.room_name.is_empty() { - len += 1; - } - if !self.transfer_to.is_empty() { - len += 1; - } - if self.play_dialtone { - len += 1; - } - if !self.headers.is_empty() { - len += 1; - } - if self.ringing_timeout.is_some() { + if !self.updates.is_empty() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.TransferSIPParticipantRequest", len)?; - if !self.participant_identity.is_empty() { - struct_ser.serialize_field("participantIdentity", &self.participant_identity)?; - } - if !self.room_name.is_empty() { - struct_ser.serialize_field("roomName", &self.room_name)?; - } - if !self.transfer_to.is_empty() { - struct_ser.serialize_field("transferTo", &self.transfer_to)?; - } - if self.play_dialtone { - struct_ser.serialize_field("playDialtone", &self.play_dialtone)?; - } - if !self.headers.is_empty() { - struct_ser.serialize_field("headers", &self.headers)?; - } - if let Some(v) = self.ringing_timeout.as_ref() { - struct_ser.serialize_field("ringingTimeout", v)?; + let mut struct_ser = serializer.serialize_struct("livekit.UpdateDataSubscription", len)?; + if !self.updates.is_empty() { + struct_ser.serialize_field("updates", &self.updates)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for TransferSipParticipantRequest { +impl<'de> serde::Deserialize<'de> for UpdateDataSubscription { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "participant_identity", - "participantIdentity", - "room_name", - "roomName", - "transfer_to", - "transferTo", - "play_dialtone", - "playDialtone", - "headers", - "ringing_timeout", - "ringingTimeout", + "updates", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - ParticipantIdentity, - RoomName, - TransferTo, - PlayDialtone, - Headers, - RingingTimeout, + Updates, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -41302,12 +43443,7 @@ impl<'de> serde::Deserialize<'de> for TransferSipParticipantRequest { E: serde::de::Error, { match value { - "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), - "roomName" | "room_name" => Ok(GeneratedField::RoomName), - "transferTo" | "transfer_to" => Ok(GeneratedField::TransferTo), - "playDialtone" | "play_dialtone" => Ok(GeneratedField::PlayDialtone), - "headers" => Ok(GeneratedField::Headers), - "ringingTimeout" | "ringing_timeout" => Ok(GeneratedField::RingingTimeout), + "updates" => Ok(GeneratedField::Updates), _ => Ok(GeneratedField::__SkipField__), } } @@ -41317,81 +43453,39 @@ impl<'de> serde::Deserialize<'de> for TransferSipParticipantRequest { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = TransferSipParticipantRequest; + type Value = UpdateDataSubscription; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.TransferSIPParticipantRequest") + formatter.write_str("struct livekit.UpdateDataSubscription") } - 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 participant_identity__ = None; - let mut room_name__ = None; - let mut transfer_to__ = None; - let mut play_dialtone__ = None; - let mut headers__ = None; - let mut ringing_timeout__ = None; + let mut updates__ = 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::RoomName => { - if room_name__.is_some() { - return Err(serde::de::Error::duplicate_field("roomName")); - } - room_name__ = Some(map_.next_value()?); - } - GeneratedField::TransferTo => { - if transfer_to__.is_some() { - return Err(serde::de::Error::duplicate_field("transferTo")); - } - transfer_to__ = Some(map_.next_value()?); - } - GeneratedField::PlayDialtone => { - if play_dialtone__.is_some() { - return Err(serde::de::Error::duplicate_field("playDialtone")); - } - play_dialtone__ = Some(map_.next_value()?); - } - GeneratedField::Headers => { - if headers__.is_some() { - return Err(serde::de::Error::duplicate_field("headers")); - } - headers__ = Some( - map_.next_value::>()? - ); - } - GeneratedField::RingingTimeout => { - if ringing_timeout__.is_some() { - return Err(serde::de::Error::duplicate_field("ringingTimeout")); + GeneratedField::Updates => { + if updates__.is_some() { + return Err(serde::de::Error::duplicate_field("updates")); } - ringing_timeout__ = map_.next_value()?; + updates__ = Some(map_.next_value()?); } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(TransferSipParticipantRequest { - participant_identity: participant_identity__.unwrap_or_default(), - room_name: room_name__.unwrap_or_default(), - transfer_to: transfer_to__.unwrap_or_default(), - play_dialtone: play_dialtone__.unwrap_or_default(), - headers: headers__.unwrap_or_default(), - ringing_timeout: ringing_timeout__, + Ok(UpdateDataSubscription { + updates: updates__.unwrap_or_default(), }) } } - deserializer.deserialize_struct("livekit.TransferSIPParticipantRequest", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.UpdateDataSubscription", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for TrickleRequest { +impl serde::Serialize for update_data_subscription::Update { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result where @@ -41399,47 +43493,46 @@ impl serde::Serialize for TrickleRequest { { use serde::ser::SerializeStruct; let mut len = 0; - if !self.candidate_init.is_empty() { + if !self.track_sid.is_empty() { len += 1; } - if self.target != 0 { + if self.subscribe { len += 1; } - if self.r#final { + if self.options.is_some() { len += 1; } - let mut struct_ser = serializer.serialize_struct("livekit.TrickleRequest", len)?; - if !self.candidate_init.is_empty() { - struct_ser.serialize_field("candidateInit", &self.candidate_init)?; + let mut struct_ser = serializer.serialize_struct("livekit.UpdateDataSubscription.Update", len)?; + if !self.track_sid.is_empty() { + struct_ser.serialize_field("trackSid", &self.track_sid)?; } - if self.target != 0 { - let v = SignalTarget::try_from(self.target) - .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.target)))?; - struct_ser.serialize_field("target", &v)?; + if self.subscribe { + struct_ser.serialize_field("subscribe", &self.subscribe)?; } - if self.r#final { - struct_ser.serialize_field("final", &self.r#final)?; + if let Some(v) = self.options.as_ref() { + struct_ser.serialize_field("options", v)?; } struct_ser.end() } } -impl<'de> serde::Deserialize<'de> for TrickleRequest { +impl<'de> serde::Deserialize<'de> for update_data_subscription::Update { #[allow(deprecated)] fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { const FIELDS: &[&str] = &[ - "candidateInit", - "target", - "final", + "track_sid", + "trackSid", + "subscribe", + "options", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { - CandidateInit, - Target, - Final, + TrackSid, + Subscribe, + Options, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -41462,9 +43555,9 @@ impl<'de> serde::Deserialize<'de> for TrickleRequest { E: serde::de::Error, { match value { - "candidateInit" => Ok(GeneratedField::CandidateInit), - "target" => Ok(GeneratedField::Target), - "final" => Ok(GeneratedField::Final), + "trackSid" | "track_sid" => Ok(GeneratedField::TrackSid), + "subscribe" => Ok(GeneratedField::Subscribe), + "options" => Ok(GeneratedField::Options), _ => Ok(GeneratedField::__SkipField__), } } @@ -41474,52 +43567,52 @@ impl<'de> serde::Deserialize<'de> for TrickleRequest { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = TrickleRequest; + type Value = update_data_subscription::Update; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.TrickleRequest") + formatter.write_str("struct livekit.UpdateDataSubscription.Update") } - 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 candidate_init__ = None; - let mut target__ = None; - let mut r#final__ = None; + let mut track_sid__ = None; + let mut subscribe__ = None; + let mut options__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::CandidateInit => { - if candidate_init__.is_some() { - return Err(serde::de::Error::duplicate_field("candidateInit")); + GeneratedField::TrackSid => { + if track_sid__.is_some() { + return Err(serde::de::Error::duplicate_field("trackSid")); } - candidate_init__ = Some(map_.next_value()?); + track_sid__ = Some(map_.next_value()?); } - GeneratedField::Target => { - if target__.is_some() { - return Err(serde::de::Error::duplicate_field("target")); + GeneratedField::Subscribe => { + if subscribe__.is_some() { + return Err(serde::de::Error::duplicate_field("subscribe")); } - target__ = Some(map_.next_value::()? as i32); + subscribe__ = Some(map_.next_value()?); } - GeneratedField::Final => { - if r#final__.is_some() { - return Err(serde::de::Error::duplicate_field("final")); + GeneratedField::Options => { + if options__.is_some() { + return Err(serde::de::Error::duplicate_field("options")); } - r#final__ = Some(map_.next_value()?); + options__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(TrickleRequest { - candidate_init: candidate_init__.unwrap_or_default(), - target: target__.unwrap_or_default(), - r#final: r#final__.unwrap_or_default(), + Ok(update_data_subscription::Update { + track_sid: track_sid__.unwrap_or_default(), + subscribe: subscribe__.unwrap_or_default(), + options: options__, }) } } - deserializer.deserialize_struct("livekit.TrickleRequest", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.UpdateDataSubscription.Update", FIELDS, GeneratedVisitor) } } impl serde::Serialize for UpdateIngressRequest { @@ -44772,6 +46865,9 @@ impl serde::Serialize for VideoLayer { if !self.rid.is_empty() { len += 1; } + if self.repair_ssrc != 0 { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.VideoLayer", len)?; if self.quality != 0 { let v = VideoQuality::try_from(self.quality) @@ -44796,6 +46892,9 @@ impl serde::Serialize for VideoLayer { if !self.rid.is_empty() { struct_ser.serialize_field("rid", &self.rid)?; } + if self.repair_ssrc != 0 { + struct_ser.serialize_field("repairSsrc", &self.repair_ssrc)?; + } struct_ser.end() } } @@ -44814,6 +46913,8 @@ impl<'de> serde::Deserialize<'de> for VideoLayer { "spatial_layer", "spatialLayer", "rid", + "repair_ssrc", + "repairSsrc", ]; #[allow(clippy::enum_variant_names)] @@ -44825,6 +46926,7 @@ impl<'de> serde::Deserialize<'de> for VideoLayer { Ssrc, SpatialLayer, Rid, + RepairSsrc, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -44854,6 +46956,7 @@ impl<'de> serde::Deserialize<'de> for VideoLayer { "ssrc" => Ok(GeneratedField::Ssrc), "spatialLayer" | "spatial_layer" => Ok(GeneratedField::SpatialLayer), "rid" => Ok(GeneratedField::Rid), + "repairSsrc" | "repair_ssrc" => Ok(GeneratedField::RepairSsrc), _ => Ok(GeneratedField::__SkipField__), } } @@ -44880,6 +46983,7 @@ impl<'de> serde::Deserialize<'de> for VideoLayer { let mut ssrc__ = None; let mut spatial_layer__ = None; let mut rid__ = None; + let mut repair_ssrc__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Quality => { @@ -44934,6 +47038,14 @@ impl<'de> serde::Deserialize<'de> for VideoLayer { } rid__ = Some(map_.next_value()?); } + GeneratedField::RepairSsrc => { + if repair_ssrc__.is_some() { + return Err(serde::de::Error::duplicate_field("repairSsrc")); + } + repair_ssrc__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -44947,6 +47059,7 @@ impl<'de> serde::Deserialize<'de> for VideoLayer { ssrc: ssrc__.unwrap_or_default(), spatial_layer: spatial_layer__.unwrap_or_default(), rid: rid__.unwrap_or_default(), + repair_ssrc: repair_ssrc__.unwrap_or_default(), }) } } @@ -46022,6 +48135,12 @@ impl serde::Serialize for WorkerMessage { worker_message::Message::MigrateJob(v) => { struct_ser.serialize_field("migrateJob", v)?; } + worker_message::Message::TextResponse(v) => { + struct_ser.serialize_field("textResponse", v)?; + } + worker_message::Message::PushText(v) => { + struct_ser.serialize_field("pushText", v)?; + } } } struct_ser.end() @@ -46045,6 +48164,10 @@ impl<'de> serde::Deserialize<'de> for WorkerMessage { "simulateJob", "migrate_job", "migrateJob", + "text_response", + "textResponse", + "push_text", + "pushText", ]; #[allow(clippy::enum_variant_names)] @@ -46056,6 +48179,8 @@ impl<'de> serde::Deserialize<'de> for WorkerMessage { Ping, SimulateJob, MigrateJob, + TextResponse, + PushText, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -46085,6 +48210,8 @@ impl<'de> serde::Deserialize<'de> for WorkerMessage { "ping" => Ok(GeneratedField::Ping), "simulateJob" | "simulate_job" => Ok(GeneratedField::SimulateJob), "migrateJob" | "migrate_job" => Ok(GeneratedField::MigrateJob), + "textResponse" | "text_response" => Ok(GeneratedField::TextResponse), + "pushText" | "push_text" => Ok(GeneratedField::PushText), _ => Ok(GeneratedField::__SkipField__), } } @@ -46154,6 +48281,20 @@ impl<'de> serde::Deserialize<'de> for WorkerMessage { return Err(serde::de::Error::duplicate_field("migrateJob")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(worker_message::Message::MigrateJob) +; + } + GeneratedField::TextResponse => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("textResponse")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(worker_message::Message::TextResponse) +; + } + GeneratedField::PushText => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("pushText")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(worker_message::Message::PushText) ; } GeneratedField::__SkipField__ => { From cb8f8a8f123150126c276e26c71be37cb29af197 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:49:24 +1100 Subject: [PATCH 230/232] Integrate --- livekit-api/src/services/connector.rs | 1 + livekit-ffi/protocol/participant.proto | 2 ++ livekit-ffi/src/conversion/participant.rs | 2 ++ livekit/src/proto.rs | 4 ++++ livekit/src/room/mod.rs | 1 + livekit/src/room/participant/mod.rs | 2 ++ 6 files changed, 12 insertions(+) diff --git a/livekit-api/src/services/connector.rs b/livekit-api/src/services/connector.rs index 551e689d8..02eec319b 100644 --- a/livekit-api/src/services/connector.rs +++ b/livekit-api/src/services/connector.rs @@ -164,6 +164,7 @@ impl ConnectorClient { proto::DisconnectWhatsAppCallRequest { whatsapp_call_id: call_id.into(), whatsapp_api_key: api_key.into(), + ..Default::default() }, self.base .auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?, diff --git a/livekit-ffi/protocol/participant.proto b/livekit-ffi/protocol/participant.proto index 589c508cf..36d1ecc27 100644 --- a/livekit-ffi/protocol/participant.proto +++ b/livekit-ffi/protocol/participant.proto @@ -44,6 +44,7 @@ enum ParticipantKind { PARTICIPANT_KIND_SIP = 3; PARTICIPANT_KIND_AGENT = 4; PARTICIPANT_KIND_CONNECTOR = 5; + PARTICIPANT_KIND_BRIDGE = 6; } enum ParticipantKindDetail { @@ -51,6 +52,7 @@ enum ParticipantKindDetail { PARTICIPANT_KIND_DETAIL_FORWARDED = 1; PARTICIPANT_KIND_DETAIL_CONNECTOR_WHATSAPP = 2; PARTICIPANT_KIND_DETAIL_CONNECTOR_TWILIO = 3; + PARTICIPANT_KIND_DETAIL_BRIDGE_RTSP = 4; } enum DisconnectReason { diff --git a/livekit-ffi/src/conversion/participant.rs b/livekit-ffi/src/conversion/participant.rs index 89cca3685..69af59b89 100644 --- a/livekit-ffi/src/conversion/participant.rs +++ b/livekit-ffi/src/conversion/participant.rs @@ -58,6 +58,7 @@ impl From for proto::ParticipantKind { ParticipantKind::Egress => proto::ParticipantKind::Egress, ParticipantKind::Agent => proto::ParticipantKind::Agent, ParticipantKind::Connector => proto::ParticipantKind::Connector, + ParticipantKind::Bridge => proto::ParticipantKind::Bridge } } } @@ -71,6 +72,7 @@ impl From for proto::ParticipantKindDetail { proto::ParticipantKindDetail::ConnectorWhatsapp } ParticipantKindDetail::ConnectorTwilio => proto::ParticipantKindDetail::ConnectorTwilio, + ParticipantKindDetail::BridgeRtsp => proto::ParticipantKindDetail::BridgeRtsp } } } diff --git a/livekit/src/proto.rs b/livekit/src/proto.rs index 20ca31e03..65575a4e7 100644 --- a/livekit/src/proto.rs +++ b/livekit/src/proto.rs @@ -155,6 +155,7 @@ impl From for participant::ParticipantKind { participant_info::Kind::Sip => participant::ParticipantKind::Sip, participant_info::Kind::Agent => participant::ParticipantKind::Agent, participant_info::Kind::Connector => participant::ParticipantKind::Connector, + participant_info::Kind::Bridge => participant::ParticipantKind::Bridge } } } @@ -174,6 +175,9 @@ impl From for participant::ParticipantKindDetail { participant_info::KindDetail::ConnectorTwilio => { participant::ParticipantKindDetail::ConnectorTwilio } + participant_info::KindDetail::BridgeRtsp => { + participant::ParticipantKindDetail::BridgeRtsp + } } } } diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index ff5276d6d..831664ebe 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -1227,6 +1227,7 @@ impl RoomSession { publish_tracks: self.local_participant.published_tracks_info(), data_channels: dcs, datachannel_receive_states: session.data_channel_receive_states(), + publish_data_tracks: Default::default() }; log::debug!("sending sync state {:?}", sync_state); diff --git a/livekit/src/room/participant/mod.rs b/livekit/src/room/participant/mod.rs index f4006644b..4fabfb89d 100644 --- a/livekit/src/room/participant/mod.rs +++ b/livekit/src/room/participant/mod.rs @@ -45,6 +45,7 @@ pub enum ParticipantKind { Sip, Agent, Connector, + Bridge } #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -53,6 +54,7 @@ pub enum ParticipantKindDetail { Forwarded, ConnectorWhatsapp, ConnectorTwilio, + BridgeRtsp } #[derive(Debug, Clone, Copy, Eq, PartialEq)] From ca66c69c827dcb0a584a82c3fb3368012b1cef1f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 04:50:29 +0000 Subject: [PATCH 231/232] generated protobuf --- .../src/proto/participant_pb.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/livekit-ffi-node-bindings/src/proto/participant_pb.ts b/livekit-ffi-node-bindings/src/proto/participant_pb.ts index 90d956f9f..ef9eb0ce9 100644 --- a/livekit-ffi-node-bindings/src/proto/participant_pb.ts +++ b/livekit-ffi-node-bindings/src/proto/participant_pb.ts @@ -55,6 +55,11 @@ export enum ParticipantKind { * @generated from enum value: PARTICIPANT_KIND_CONNECTOR = 5; */ CONNECTOR = 5, + + /** + * @generated from enum value: PARTICIPANT_KIND_BRIDGE = 6; + */ + BRIDGE = 6, } // Retrieve enum metadata with: proto2.getEnumType(ParticipantKind) proto2.util.setEnumType(ParticipantKind, "livekit.proto.ParticipantKind", [ @@ -64,6 +69,7 @@ proto2.util.setEnumType(ParticipantKind, "livekit.proto.ParticipantKind", [ { no: 3, name: "PARTICIPANT_KIND_SIP" }, { no: 4, name: "PARTICIPANT_KIND_AGENT" }, { no: 5, name: "PARTICIPANT_KIND_CONNECTOR" }, + { no: 6, name: "PARTICIPANT_KIND_BRIDGE" }, ]); /** @@ -89,6 +95,11 @@ export enum ParticipantKindDetail { * @generated from enum value: PARTICIPANT_KIND_DETAIL_CONNECTOR_TWILIO = 3; */ CONNECTOR_TWILIO = 3, + + /** + * @generated from enum value: PARTICIPANT_KIND_DETAIL_BRIDGE_RTSP = 4; + */ + BRIDGE_RTSP = 4, } // Retrieve enum metadata with: proto2.getEnumType(ParticipantKindDetail) proto2.util.setEnumType(ParticipantKindDetail, "livekit.proto.ParticipantKindDetail", [ @@ -96,6 +107,7 @@ proto2.util.setEnumType(ParticipantKindDetail, "livekit.proto.ParticipantKindDet { no: 1, name: "PARTICIPANT_KIND_DETAIL_FORWARDED" }, { no: 2, name: "PARTICIPANT_KIND_DETAIL_CONNECTOR_WHATSAPP" }, { no: 3, name: "PARTICIPANT_KIND_DETAIL_CONNECTOR_TWILIO" }, + { no: 4, name: "PARTICIPANT_KIND_DETAIL_BRIDGE_RTSP" }, ]); /** From eef2550659b0b96a55c2a9efb23ef26058ad4fc6 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 05:10:05 +0000 Subject: [PATCH 232/232] generated protobuf --- livekit-protocol/src/livekit.rs | 124 +---- livekit-protocol/src/livekit.serde.rs | 668 -------------------------- 2 files changed, 10 insertions(+), 782 deletions(-) diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index fe99a4674..4b852e825 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -88,8 +88,6 @@ pub struct MetricsRecordingHeader { pub duration: u64, #[prost(message, optional, tag="4")] pub start_time: ::core::option::Option<::pbjson_types::Timestamp>, - #[prost(map="string, string", tag="5")] - pub room_tags: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>, } // // Protocol used to record metrics for a specific session. @@ -414,11 +412,9 @@ pub mod participant_info { /// LiveKit agents Agent = 4, /// Connectors participants - Connector = 7, - /// Bridge participants /// - /// NEXT_ID: 9 - Bridge = 8, + /// NEXT_ID: 8 + Connector = 7, } impl Kind { /// String value of the enum field names used in the ProtoBuf definition. @@ -433,7 +429,6 @@ pub mod participant_info { Kind::Sip => "SIP", Kind::Agent => "AGENT", Kind::Connector => "CONNECTOR", - Kind::Bridge => "BRIDGE", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -445,7 +440,6 @@ pub mod participant_info { "SIP" => Some(Self::Sip), "AGENT" => Some(Self::Agent), "CONNECTOR" => Some(Self::Connector), - "BRIDGE" => Some(Self::Bridge), _ => None, } } @@ -457,8 +451,6 @@ pub mod participant_info { Forwarded = 1, ConnectorWhatsapp = 2, ConnectorTwilio = 3, - /// NEXT_ID: 5 - BridgeRtsp = 4, } impl KindDetail { /// String value of the enum field names used in the ProtoBuf definition. @@ -471,7 +463,6 @@ pub mod participant_info { KindDetail::Forwarded => "FORWARDED", KindDetail::ConnectorWhatsapp => "CONNECTOR_WHATSAPP", KindDetail::ConnectorTwilio => "CONNECTOR_TWILIO", - KindDetail::BridgeRtsp => "BRIDGE_RTSP", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -481,7 +472,6 @@ pub mod participant_info { "FORWARDED" => Some(Self::Forwarded), "CONNECTOR_WHATSAPP" => Some(Self::ConnectorWhatsapp), "CONNECTOR_TWILIO" => Some(Self::ConnectorTwilio), - "BRIDGE_RTSP" => Some(Self::BridgeRtsp), _ => None, } } @@ -657,8 +647,6 @@ pub struct VideoLayer { pub spatial_layer: i32, #[prost(string, tag="7")] pub rid: ::prost::alloc::string::String, - #[prost(uint32, tag="8")] - pub repair_ssrc: u32, } /// Nested message and enum types in `VideoLayer`. pub mod video_layer { @@ -4027,8 +4015,6 @@ pub struct ConnectionSettings { pub subscriber_allow_pause: ::core::option::Option, #[prost(bool, tag="4")] pub disable_ice_lite: bool, - #[prost(bool, optional, tag="5")] - pub auto_subscribe_data_track: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -4234,7 +4220,7 @@ pub struct JobState { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct WorkerMessage { - #[prost(oneof="worker_message::Message", tags="1, 2, 3, 4, 5, 6, 7, 8, 9")] + #[prost(oneof="worker_message::Message", tags="1, 2, 3, 4, 5, 6, 7")] pub message: ::core::option::Option, } /// Nested message and enum types in `WorkerMessage`. @@ -4260,17 +4246,13 @@ pub mod worker_message { SimulateJob(super::SimulateJobRequest), #[prost(message, tag="7")] MigrateJob(super::MigrateJobRequest), - #[prost(message, tag="8")] - TextResponse(super::TextMessageResponse), - #[prost(message, tag="9")] - PushText(super::PushTextRequest), } } /// from Server to Worker #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ServerMessage { - #[prost(oneof="server_message::Message", tags="1, 2, 3, 5, 4, 6")] + #[prost(oneof="server_message::Message", tags="1, 2, 3, 5, 4")] pub message: ::core::option::Option, } /// Nested message and enum types in `ServerMessage`. @@ -4290,8 +4272,6 @@ pub mod server_message { Termination(super::JobTermination), #[prost(message, tag="4")] Pong(super::WorkerPong), - #[prost(message, tag="6")] - TextRequest(super::TextMessageRequest), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -4421,42 +4401,6 @@ pub struct JobTermination { #[prost(string, tag="1")] pub job_id: ::prost::alloc::string::String, } -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TextMessageRequest { - #[prost(string, tag="1")] - pub message_id: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub session_id: ::prost::alloc::string::String, - #[prost(string, tag="3")] - pub agent_name: ::prost::alloc::string::String, - #[prost(string, tag="4")] - pub metadata: ::prost::alloc::string::String, - #[prost(bytes="vec", tag="5")] - pub session_data: ::prost::alloc::vec::Vec, - #[prost(string, tag="6")] - pub text: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct PushTextRequest { - /// The message_id of the TextMessageRequest that this push is for - #[prost(string, tag="1")] - pub message_id: ::prost::alloc::string::String, - #[prost(string, tag="2")] - pub content: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TextMessageResponse { - /// Indicate the request is completed - #[prost(string, tag="1")] - pub message_id: ::prost::alloc::string::String, - #[prost(bytes="vec", tag="2")] - pub session_data: ::prost::alloc::vec::Vec, - #[prost(string, tag="3")] - pub error: ::prost::alloc::string::String, -} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum JobType { @@ -4603,7 +4547,7 @@ pub struct AgentDispatch { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AgentDispatchState { - /// For dispatches of tyoe JT_ROOM, there will be at most 1 job. + /// For dispatches of tyoe JT_ROOM, there will be at most 1 job. /// For dispatches of type JT_PUBLISHER, there will be 1 per publisher. #[prost(message, repeated, tag="1")] pub jobs: ::prost::alloc::vec::Vec, @@ -5907,12 +5851,9 @@ pub struct SipDispatchRuleInfo { pub trunk_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(bool, tag="4")] pub hide_phone_number: bool, - /// Dispatch Rule will only accept a call made from these numbers (if set). + /// Dispatch Rule will only accept a call made to these numbers (if set). #[prost(string, repeated, tag="7")] pub inbound_numbers: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - /// Dispatch Rule will only accept a call made to these numbers (if set). - #[prost(string, repeated, tag="13")] - pub numbers: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, /// Human-readable name for the Dispatch Rule. #[prost(string, tag="5")] pub name: ::prost::alloc::string::String, @@ -5932,7 +5873,7 @@ pub struct SipDispatchRuleInfo { pub room_config: ::core::option::Option, #[prost(bool, tag="11")] pub krisp_enabled: bool, - /// NEXT ID: 14 + /// NEXT ID: 13 #[prost(enumeration="SipMediaEncryption", tag="12")] pub media_encryption: i32, } @@ -6179,8 +6120,6 @@ pub struct SipCallInfo { pub call_context: ::prost::alloc::vec::Vec<::pbjson_types::Any>, #[prost(message, optional, tag="27")] pub provider_info: ::core::option::Option, - #[prost(string, tag="28")] - pub sip_call_id: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -6728,7 +6667,7 @@ impl SipCallDirection { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DialWhatsAppCallRequest { - /// Required - The phone number id of the business that is initiating the call + /// Required - The number of the business that is initiating the call #[prost(string, tag="1")] pub whatsapp_phone_number_id: ::prost::alloc::string::String, /// Required - The number of the user that is supossed to receive the call @@ -6750,7 +6689,6 @@ pub struct DialWhatsAppCallRequest { #[prost(message, repeated, tag="6")] pub agents: ::prost::alloc::vec::Vec, /// Optional - Identity of the participant in LiveKit room - /// This is used for logging purposes, so it is advised to not put PII in this field. #[prost(string, tag="7")] pub participant_identity: ::prost::alloc::string::String, /// Optional - Name of the participant in LiveKit room @@ -6784,49 +6722,9 @@ pub struct DisconnectWhatsAppCallRequest { /// Required - Call ID sent by Meta #[prost(string, tag="1")] pub whatsapp_call_id: ::prost::alloc::string::String, - /// The API key of the whatsapp business. - /// Required if the DisconnectReason is BUSINESS_INITIATED. - /// Optional for USER_INITIATED as no API call to WhatsApp is needed. + /// Required - The API key of the business that is disconnecting the call #[prost(string, tag="2")] pub whatsapp_api_key: ::prost::alloc::string::String, - /// The reason for disconnecting the call - #[prost(enumeration="disconnect_whats_app_call_request::DisconnectReason", tag="3")] - pub disconnect_reason: i32, -} -/// Nested message and enum types in `DisconnectWhatsAppCallRequest`. -pub mod disconnect_whats_app_call_request { - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] - #[repr(i32)] - pub enum DisconnectReason { - /// The call is being disconnected by the business - BusinessInitiated = 0, - /// The call is disconnected by the user. - /// This can be tracked as part of call terminate webhook - /// - /// Note that this webhook will also be sent when the call is disconnected by the business. - /// Calling the API twice in such cases will result in an error. - UserInitiated = 1, - } - impl DisconnectReason { - /// 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 { - DisconnectReason::BusinessInitiated => "BUSINESS_INITIATED", - DisconnectReason::UserInitiated => "USER_INITIATED", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "BUSINESS_INITIATED" => Some(Self::BusinessInitiated), - "USER_INITIATED" => Some(Self::UserInitiated), - _ => None, - } - } - } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -6850,7 +6748,7 @@ pub struct ConnectWhatsAppCallResponse { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AcceptWhatsAppCallRequest { - /// Required - The phone number id of the business that is conencting the call + /// Required - The number of the business that is conencting the call #[prost(string, tag="1")] pub whatsapp_phone_number_id: ::prost::alloc::string::String, /// Required - The API key of the business that is connecting the call @@ -6876,7 +6774,6 @@ pub struct AcceptWhatsAppCallRequest { #[prost(message, repeated, tag="7")] pub agents: ::prost::alloc::vec::Vec, /// Optional - Identity of the participant in LiveKit room - /// This is used for logging purposes, so it is advised to not put PII in this field. #[prost(string, tag="8")] pub participant_identity: ::prost::alloc::string::String, /// Optional - Name of the participant in LiveKit room @@ -6950,7 +6847,6 @@ pub struct ConnectTwilioCallRequest { #[prost(message, repeated, tag="3")] pub agents: ::prost::alloc::vec::Vec, /// Optional identity of the participant in LiveKit room - /// This is used for logging purposes, so it is advised to not put PII in this field. #[prost(string, tag="4")] pub participant_identity: ::prost::alloc::string::String, /// Optional name of the participant in LiveKit room diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index 7739e401e..1b4660704 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -4364,9 +4364,6 @@ impl serde::Serialize for ConnectionSettings { if self.disable_ice_lite { len += 1; } - if self.auto_subscribe_data_track.is_some() { - len += 1; - } let mut struct_ser = serializer.serialize_struct("livekit.ConnectionSettings", len)?; if self.auto_subscribe { struct_ser.serialize_field("autoSubscribe", &self.auto_subscribe)?; @@ -4380,9 +4377,6 @@ impl serde::Serialize for ConnectionSettings { if self.disable_ice_lite { struct_ser.serialize_field("disableIceLite", &self.disable_ice_lite)?; } - if let Some(v) = self.auto_subscribe_data_track.as_ref() { - struct_ser.serialize_field("autoSubscribeDataTrack", v)?; - } struct_ser.end() } } @@ -4401,8 +4395,6 @@ impl<'de> serde::Deserialize<'de> for ConnectionSettings { "subscriberAllowPause", "disable_ice_lite", "disableIceLite", - "auto_subscribe_data_track", - "autoSubscribeDataTrack", ]; #[allow(clippy::enum_variant_names)] @@ -4411,7 +4403,6 @@ impl<'de> serde::Deserialize<'de> for ConnectionSettings { AdaptiveStream, SubscriberAllowPause, DisableIceLite, - AutoSubscribeDataTrack, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -4438,7 +4429,6 @@ impl<'de> serde::Deserialize<'de> for ConnectionSettings { "adaptiveStream" | "adaptive_stream" => Ok(GeneratedField::AdaptiveStream), "subscriberAllowPause" | "subscriber_allow_pause" => Ok(GeneratedField::SubscriberAllowPause), "disableIceLite" | "disable_ice_lite" => Ok(GeneratedField::DisableIceLite), - "autoSubscribeDataTrack" | "auto_subscribe_data_track" => Ok(GeneratedField::AutoSubscribeDataTrack), _ => Ok(GeneratedField::__SkipField__), } } @@ -4462,7 +4452,6 @@ impl<'de> serde::Deserialize<'de> for ConnectionSettings { let mut adaptive_stream__ = None; let mut subscriber_allow_pause__ = None; let mut disable_ice_lite__ = None; - let mut auto_subscribe_data_track__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::AutoSubscribe => { @@ -4489,12 +4478,6 @@ impl<'de> serde::Deserialize<'de> for ConnectionSettings { } disable_ice_lite__ = Some(map_.next_value()?); } - GeneratedField::AutoSubscribeDataTrack => { - if auto_subscribe_data_track__.is_some() { - return Err(serde::de::Error::duplicate_field("autoSubscribeDataTrack")); - } - auto_subscribe_data_track__ = map_.next_value()?; - } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -4505,7 +4488,6 @@ impl<'de> serde::Deserialize<'de> for ConnectionSettings { adaptive_stream: adaptive_stream__.unwrap_or_default(), subscriber_allow_pause: subscriber_allow_pause__, disable_ice_lite: disable_ice_lite__.unwrap_or_default(), - auto_subscribe_data_track: auto_subscribe_data_track__, }) } } @@ -10360,9 +10342,6 @@ impl serde::Serialize for DisconnectWhatsAppCallRequest { if !self.whatsapp_api_key.is_empty() { len += 1; } - if self.disconnect_reason != 0 { - len += 1; - } let mut struct_ser = serializer.serialize_struct("livekit.DisconnectWhatsAppCallRequest", len)?; if !self.whatsapp_call_id.is_empty() { struct_ser.serialize_field("whatsappCallId", &self.whatsapp_call_id)?; @@ -10370,11 +10349,6 @@ impl serde::Serialize for DisconnectWhatsAppCallRequest { if !self.whatsapp_api_key.is_empty() { struct_ser.serialize_field("whatsappApiKey", &self.whatsapp_api_key)?; } - if self.disconnect_reason != 0 { - let v = disconnect_whats_app_call_request::DisconnectReason::try_from(self.disconnect_reason) - .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.disconnect_reason)))?; - struct_ser.serialize_field("disconnectReason", &v)?; - } struct_ser.end() } } @@ -10389,15 +10363,12 @@ impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallRequest { "whatsappCallId", "whatsapp_api_key", "whatsappApiKey", - "disconnect_reason", - "disconnectReason", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { WhatsappCallId, WhatsappApiKey, - DisconnectReason, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -10422,7 +10393,6 @@ impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallRequest { match value { "whatsappCallId" | "whatsapp_call_id" => Ok(GeneratedField::WhatsappCallId), "whatsappApiKey" | "whatsapp_api_key" => Ok(GeneratedField::WhatsappApiKey), - "disconnectReason" | "disconnect_reason" => Ok(GeneratedField::DisconnectReason), _ => Ok(GeneratedField::__SkipField__), } } @@ -10444,7 +10414,6 @@ impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallRequest { { let mut whatsapp_call_id__ = None; let mut whatsapp_api_key__ = None; - let mut disconnect_reason__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::WhatsappCallId => { @@ -10459,12 +10428,6 @@ impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallRequest { } whatsapp_api_key__ = Some(map_.next_value()?); } - GeneratedField::DisconnectReason => { - if disconnect_reason__.is_some() { - return Err(serde::de::Error::duplicate_field("disconnectReason")); - } - disconnect_reason__ = Some(map_.next_value::()? as i32); - } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -10473,84 +10436,12 @@ impl<'de> serde::Deserialize<'de> for DisconnectWhatsAppCallRequest { Ok(DisconnectWhatsAppCallRequest { whatsapp_call_id: whatsapp_call_id__.unwrap_or_default(), whatsapp_api_key: whatsapp_api_key__.unwrap_or_default(), - disconnect_reason: disconnect_reason__.unwrap_or_default(), }) } } deserializer.deserialize_struct("livekit.DisconnectWhatsAppCallRequest", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for disconnect_whats_app_call_request::DisconnectReason { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - let variant = match self { - Self::BusinessInitiated => "BUSINESS_INITIATED", - Self::UserInitiated => "USER_INITIATED", - }; - serializer.serialize_str(variant) - } -} -impl<'de> serde::Deserialize<'de> for disconnect_whats_app_call_request::DisconnectReason { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "BUSINESS_INITIATED", - "USER_INITIATED", - ]; - - struct GeneratedVisitor; - - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = disconnect_whats_app_call_request::DisconnectReason; - - 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 { - "BUSINESS_INITIATED" => Ok(disconnect_whats_app_call_request::DisconnectReason::BusinessInitiated), - "USER_INITIATED" => Ok(disconnect_whats_app_call_request::DisconnectReason::UserInitiated), - _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), - } - } - } - deserializer.deserialize_any(GeneratedVisitor) - } -} impl serde::Serialize for DisconnectWhatsAppCallResponse { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -20643,9 +20534,6 @@ impl serde::Serialize for MetricsRecordingHeader { if self.start_time.is_some() { len += 1; } - if !self.room_tags.is_empty() { - len += 1; - } let mut struct_ser = serializer.serialize_struct("livekit.MetricsRecordingHeader", len)?; if !self.room_id.is_empty() { struct_ser.serialize_field("roomId", &self.room_id)?; @@ -20658,9 +20546,6 @@ impl serde::Serialize for MetricsRecordingHeader { if let Some(v) = self.start_time.as_ref() { struct_ser.serialize_field("startTime", v)?; } - if !self.room_tags.is_empty() { - struct_ser.serialize_field("roomTags", &self.room_tags)?; - } struct_ser.end() } } @@ -20676,8 +20561,6 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { "duration", "start_time", "startTime", - "room_tags", - "roomTags", ]; #[allow(clippy::enum_variant_names)] @@ -20685,7 +20568,6 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { RoomId, Duration, StartTime, - RoomTags, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -20711,7 +20593,6 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { "roomId" | "room_id" => Ok(GeneratedField::RoomId), "duration" => Ok(GeneratedField::Duration), "startTime" | "start_time" => Ok(GeneratedField::StartTime), - "roomTags" | "room_tags" => Ok(GeneratedField::RoomTags), _ => Ok(GeneratedField::__SkipField__), } } @@ -20734,7 +20615,6 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { let mut room_id__ = None; let mut duration__ = None; let mut start_time__ = None; - let mut room_tags__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::RoomId => { @@ -20757,14 +20637,6 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { } start_time__ = map_.next_value()?; } - GeneratedField::RoomTags => { - if room_tags__.is_some() { - return Err(serde::de::Error::duplicate_field("roomTags")); - } - room_tags__ = Some( - map_.next_value::>()? - ); - } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -20774,7 +20646,6 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { room_id: room_id__.unwrap_or_default(), duration: duration__.unwrap_or_default(), start_time: start_time__, - room_tags: room_tags__.unwrap_or_default(), }) } } @@ -22212,7 +22083,6 @@ impl serde::Serialize for participant_info::Kind { Self::Sip => "SIP", Self::Agent => "AGENT", Self::Connector => "CONNECTOR", - Self::Bridge => "BRIDGE", }; serializer.serialize_str(variant) } @@ -22230,7 +22100,6 @@ impl<'de> serde::Deserialize<'de> for participant_info::Kind { "SIP", "AGENT", "CONNECTOR", - "BRIDGE", ]; struct GeneratedVisitor; @@ -22277,7 +22146,6 @@ impl<'de> serde::Deserialize<'de> for participant_info::Kind { "SIP" => Ok(participant_info::Kind::Sip), "AGENT" => Ok(participant_info::Kind::Agent), "CONNECTOR" => Ok(participant_info::Kind::Connector), - "BRIDGE" => Ok(participant_info::Kind::Bridge), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -22296,7 +22164,6 @@ impl serde::Serialize for participant_info::KindDetail { Self::Forwarded => "FORWARDED", Self::ConnectorWhatsapp => "CONNECTOR_WHATSAPP", Self::ConnectorTwilio => "CONNECTOR_TWILIO", - Self::BridgeRtsp => "BRIDGE_RTSP", }; serializer.serialize_str(variant) } @@ -22312,7 +22179,6 @@ impl<'de> serde::Deserialize<'de> for participant_info::KindDetail { "FORWARDED", "CONNECTOR_WHATSAPP", "CONNECTOR_TWILIO", - "BRIDGE_RTSP", ]; struct GeneratedVisitor; @@ -22357,7 +22223,6 @@ impl<'de> serde::Deserialize<'de> for participant_info::KindDetail { "FORWARDED" => Ok(participant_info::KindDetail::Forwarded), "CONNECTOR_WHATSAPP" => Ok(participant_info::KindDetail::ConnectorWhatsapp), "CONNECTOR_TWILIO" => Ok(participant_info::KindDetail::ConnectorTwilio), - "BRIDGE_RTSP" => Ok(participant_info::KindDetail::BridgeRtsp), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -24109,119 +23974,6 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackResponse { deserializer.deserialize_struct("livekit.PublishDataTrackResponse", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for PushTextRequest { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut len = 0; - if !self.message_id.is_empty() { - len += 1; - } - if !self.content.is_empty() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.PushTextRequest", len)?; - if !self.message_id.is_empty() { - struct_ser.serialize_field("messageId", &self.message_id)?; - } - if !self.content.is_empty() { - struct_ser.serialize_field("content", &self.content)?; - } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for PushTextRequest { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "message_id", - "messageId", - "content", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - MessageId, - Content, - __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 { - "messageId" | "message_id" => Ok(GeneratedField::MessageId), - "content" => Ok(GeneratedField::Content), - _ => Ok(GeneratedField::__SkipField__), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = PushTextRequest; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.PushTextRequest") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut message_id__ = None; - let mut content__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::MessageId => { - if message_id__.is_some() { - return Err(serde::de::Error::duplicate_field("messageId")); - } - message_id__ = Some(map_.next_value()?); - } - GeneratedField::Content => { - if content__.is_some() { - return Err(serde::de::Error::duplicate_field("content")); - } - content__ = Some(map_.next_value()?); - } - GeneratedField::__SkipField__ => { - let _ = map_.next_value::()?; - } - } - } - Ok(PushTextRequest { - message_id: message_id__.unwrap_or_default(), - content: content__.unwrap_or_default(), - }) - } - } - deserializer.deserialize_struct("livekit.PushTextRequest", FIELDS, GeneratedVisitor) - } -} impl serde::Serialize for RtcpSenderReportState { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -29951,9 +29703,6 @@ impl serde::Serialize for SipCallInfo { if self.provider_info.is_some() { len += 1; } - if !self.sip_call_id.is_empty() { - len += 1; - } let mut struct_ser = serializer.serialize_struct("livekit.SIPCallInfo", len)?; if !self.call_id.is_empty() { struct_ser.serialize_field("callId", &self.call_id)?; @@ -30058,9 +29807,6 @@ impl serde::Serialize for SipCallInfo { if let Some(v) = self.provider_info.as_ref() { struct_ser.serialize_field("providerInfo", v)?; } - if !self.sip_call_id.is_empty() { - struct_ser.serialize_field("sipCallId", &self.sip_call_id)?; - } struct_ser.end() } } @@ -30123,8 +29869,6 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { "callContext", "provider_info", "providerInfo", - "sip_call_id", - "sipCallId", ]; #[allow(clippy::enum_variant_names)] @@ -30156,7 +29900,6 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { PcapFileLink, CallContext, ProviderInfo, - SipCallId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -30206,7 +29949,6 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { "pcapFileLink" | "pcap_file_link" => Ok(GeneratedField::PcapFileLink), "callContext" | "call_context" => Ok(GeneratedField::CallContext), "providerInfo" | "provider_info" => Ok(GeneratedField::ProviderInfo), - "sipCallId" | "sip_call_id" => Ok(GeneratedField::SipCallId), _ => Ok(GeneratedField::__SkipField__), } } @@ -30253,7 +29995,6 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { let mut pcap_file_link__ = None; let mut call_context__ = None; let mut provider_info__ = None; - let mut sip_call_id__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::CallId => { @@ -30432,12 +30173,6 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { } provider_info__ = map_.next_value()?; } - GeneratedField::SipCallId => { - if sip_call_id__.is_some() { - return Err(serde::de::Error::duplicate_field("sipCallId")); - } - sip_call_id__ = Some(map_.next_value()?); - } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -30471,7 +30206,6 @@ impl<'de> serde::Deserialize<'de> for SipCallInfo { pcap_file_link: pcap_file_link__.unwrap_or_default(), call_context: call_context__.unwrap_or_default(), provider_info: provider_info__, - sip_call_id: sip_call_id__.unwrap_or_default(), }) } } @@ -31066,9 +30800,6 @@ impl serde::Serialize for SipDispatchRuleInfo { if !self.inbound_numbers.is_empty() { len += 1; } - if !self.numbers.is_empty() { - len += 1; - } if !self.name.is_empty() { len += 1; } @@ -31106,9 +30837,6 @@ impl serde::Serialize for SipDispatchRuleInfo { if !self.inbound_numbers.is_empty() { struct_ser.serialize_field("inboundNumbers", &self.inbound_numbers)?; } - if !self.numbers.is_empty() { - struct_ser.serialize_field("numbers", &self.numbers)?; - } if !self.name.is_empty() { struct_ser.serialize_field("name", &self.name)?; } @@ -31151,7 +30879,6 @@ impl<'de> serde::Deserialize<'de> for SipDispatchRuleInfo { "hidePhoneNumber", "inbound_numbers", "inboundNumbers", - "numbers", "name", "metadata", "attributes", @@ -31172,7 +30899,6 @@ impl<'de> serde::Deserialize<'de> for SipDispatchRuleInfo { TrunkIds, HidePhoneNumber, InboundNumbers, - Numbers, Name, Metadata, Attributes, @@ -31207,7 +30933,6 @@ impl<'de> serde::Deserialize<'de> for SipDispatchRuleInfo { "trunkIds" | "trunk_ids" => Ok(GeneratedField::TrunkIds), "hidePhoneNumber" | "hide_phone_number" => Ok(GeneratedField::HidePhoneNumber), "inboundNumbers" | "inbound_numbers" => Ok(GeneratedField::InboundNumbers), - "numbers" => Ok(GeneratedField::Numbers), "name" => Ok(GeneratedField::Name), "metadata" => Ok(GeneratedField::Metadata), "attributes" => Ok(GeneratedField::Attributes), @@ -31239,7 +30964,6 @@ impl<'de> serde::Deserialize<'de> for SipDispatchRuleInfo { let mut trunk_ids__ = None; let mut hide_phone_number__ = None; let mut inbound_numbers__ = None; - let mut numbers__ = None; let mut name__ = None; let mut metadata__ = None; let mut attributes__ = None; @@ -31279,12 +31003,6 @@ impl<'de> serde::Deserialize<'de> for SipDispatchRuleInfo { } inbound_numbers__ = Some(map_.next_value()?); } - GeneratedField::Numbers => { - if numbers__.is_some() { - return Err(serde::de::Error::duplicate_field("numbers")); - } - numbers__ = Some(map_.next_value()?); - } GeneratedField::Name => { if name__.is_some() { return Err(serde::de::Error::duplicate_field("name")); @@ -31340,7 +31058,6 @@ impl<'de> serde::Deserialize<'de> for SipDispatchRuleInfo { trunk_ids: trunk_ids__.unwrap_or_default(), hide_phone_number: hide_phone_number__.unwrap_or_default(), inbound_numbers: inbound_numbers__.unwrap_or_default(), - numbers: numbers__.unwrap_or_default(), name: name__.unwrap_or_default(), metadata: metadata__.unwrap_or_default(), attributes: attributes__.unwrap_or_default(), @@ -35902,9 +35619,6 @@ impl serde::Serialize for ServerMessage { server_message::Message::Pong(v) => { struct_ser.serialize_field("pong", v)?; } - server_message::Message::TextRequest(v) => { - struct_ser.serialize_field("textRequest", v)?; - } } } struct_ser.end() @@ -35922,8 +35636,6 @@ impl<'de> serde::Deserialize<'de> for ServerMessage { "assignment", "termination", "pong", - "text_request", - "textRequest", ]; #[allow(clippy::enum_variant_names)] @@ -35933,7 +35645,6 @@ impl<'de> serde::Deserialize<'de> for ServerMessage { Assignment, Termination, Pong, - TextRequest, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -35961,7 +35672,6 @@ impl<'de> serde::Deserialize<'de> for ServerMessage { "assignment" => Ok(GeneratedField::Assignment), "termination" => Ok(GeneratedField::Termination), "pong" => Ok(GeneratedField::Pong), - "textRequest" | "text_request" => Ok(GeneratedField::TextRequest), _ => Ok(GeneratedField::__SkipField__), } } @@ -36017,13 +35727,6 @@ impl<'de> serde::Deserialize<'de> for ServerMessage { return Err(serde::de::Error::duplicate_field("pong")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(server_message::Message::Pong) -; - } - GeneratedField::TextRequest => { - if message__.is_some() { - return Err(serde::de::Error::duplicate_field("textRequest")); - } - message__ = map_.next_value::<::std::option::Option<_>>()?.map(server_message::Message::TextRequest) ; } GeneratedField::__SkipField__ => { @@ -40330,329 +40033,6 @@ impl<'de> serde::Deserialize<'de> for SyncState { deserializer.deserialize_struct("livekit.SyncState", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for TextMessageRequest { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut len = 0; - if !self.message_id.is_empty() { - len += 1; - } - if !self.session_id.is_empty() { - len += 1; - } - if !self.agent_name.is_empty() { - len += 1; - } - if !self.metadata.is_empty() { - len += 1; - } - if !self.session_data.is_empty() { - len += 1; - } - if !self.text.is_empty() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.TextMessageRequest", len)?; - if !self.message_id.is_empty() { - struct_ser.serialize_field("messageId", &self.message_id)?; - } - if !self.session_id.is_empty() { - struct_ser.serialize_field("sessionId", &self.session_id)?; - } - if !self.agent_name.is_empty() { - struct_ser.serialize_field("agentName", &self.agent_name)?; - } - if !self.metadata.is_empty() { - struct_ser.serialize_field("metadata", &self.metadata)?; - } - if !self.session_data.is_empty() { - #[allow(clippy::needless_borrow)] - #[allow(clippy::needless_borrows_for_generic_args)] - struct_ser.serialize_field("sessionData", pbjson::private::base64::encode(&self.session_data).as_str())?; - } - if !self.text.is_empty() { - struct_ser.serialize_field("text", &self.text)?; - } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for TextMessageRequest { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "message_id", - "messageId", - "session_id", - "sessionId", - "agent_name", - "agentName", - "metadata", - "session_data", - "sessionData", - "text", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - MessageId, - SessionId, - AgentName, - Metadata, - SessionData, - Text, - __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 { - "messageId" | "message_id" => Ok(GeneratedField::MessageId), - "sessionId" | "session_id" => Ok(GeneratedField::SessionId), - "agentName" | "agent_name" => Ok(GeneratedField::AgentName), - "metadata" => Ok(GeneratedField::Metadata), - "sessionData" | "session_data" => Ok(GeneratedField::SessionData), - "text" => Ok(GeneratedField::Text), - _ => Ok(GeneratedField::__SkipField__), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = TextMessageRequest; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.TextMessageRequest") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut message_id__ = None; - let mut session_id__ = None; - let mut agent_name__ = None; - let mut metadata__ = None; - let mut session_data__ = None; - let mut text__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::MessageId => { - if message_id__.is_some() { - return Err(serde::de::Error::duplicate_field("messageId")); - } - message_id__ = Some(map_.next_value()?); - } - GeneratedField::SessionId => { - if session_id__.is_some() { - return Err(serde::de::Error::duplicate_field("sessionId")); - } - session_id__ = Some(map_.next_value()?); - } - GeneratedField::AgentName => { - if agent_name__.is_some() { - return Err(serde::de::Error::duplicate_field("agentName")); - } - agent_name__ = Some(map_.next_value()?); - } - GeneratedField::Metadata => { - if metadata__.is_some() { - return Err(serde::de::Error::duplicate_field("metadata")); - } - metadata__ = Some(map_.next_value()?); - } - GeneratedField::SessionData => { - if session_data__.is_some() { - return Err(serde::de::Error::duplicate_field("sessionData")); - } - session_data__ = - Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) - ; - } - GeneratedField::Text => { - if text__.is_some() { - return Err(serde::de::Error::duplicate_field("text")); - } - text__ = Some(map_.next_value()?); - } - GeneratedField::__SkipField__ => { - let _ = map_.next_value::()?; - } - } - } - Ok(TextMessageRequest { - message_id: message_id__.unwrap_or_default(), - session_id: session_id__.unwrap_or_default(), - agent_name: agent_name__.unwrap_or_default(), - metadata: metadata__.unwrap_or_default(), - session_data: session_data__.unwrap_or_default(), - text: text__.unwrap_or_default(), - }) - } - } - deserializer.deserialize_struct("livekit.TextMessageRequest", FIELDS, GeneratedVisitor) - } -} -impl serde::Serialize for TextMessageResponse { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut len = 0; - if !self.message_id.is_empty() { - len += 1; - } - if !self.session_data.is_empty() { - len += 1; - } - if !self.error.is_empty() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.TextMessageResponse", len)?; - if !self.message_id.is_empty() { - struct_ser.serialize_field("messageId", &self.message_id)?; - } - if !self.session_data.is_empty() { - #[allow(clippy::needless_borrow)] - #[allow(clippy::needless_borrows_for_generic_args)] - struct_ser.serialize_field("sessionData", pbjson::private::base64::encode(&self.session_data).as_str())?; - } - if !self.error.is_empty() { - struct_ser.serialize_field("error", &self.error)?; - } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for TextMessageResponse { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "message_id", - "messageId", - "session_data", - "sessionData", - "error", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - MessageId, - SessionData, - Error, - __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 { - "messageId" | "message_id" => Ok(GeneratedField::MessageId), - "sessionData" | "session_data" => Ok(GeneratedField::SessionData), - "error" => Ok(GeneratedField::Error), - _ => Ok(GeneratedField::__SkipField__), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = TextMessageResponse; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.TextMessageResponse") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut message_id__ = None; - let mut session_data__ = None; - let mut error__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::MessageId => { - if message_id__.is_some() { - return Err(serde::de::Error::duplicate_field("messageId")); - } - message_id__ = Some(map_.next_value()?); - } - GeneratedField::SessionData => { - if session_data__.is_some() { - return Err(serde::de::Error::duplicate_field("sessionData")); - } - session_data__ = - Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) - ; - } - GeneratedField::Error => { - if error__.is_some() { - return Err(serde::de::Error::duplicate_field("error")); - } - error__ = Some(map_.next_value()?); - } - GeneratedField::__SkipField__ => { - let _ = map_.next_value::()?; - } - } - } - Ok(TextMessageResponse { - message_id: message_id__.unwrap_or_default(), - session_data: session_data__.unwrap_or_default(), - error: error__.unwrap_or_default(), - }) - } - } - deserializer.deserialize_struct("livekit.TextMessageResponse", FIELDS, GeneratedVisitor) - } -} impl serde::Serialize for TimeSeriesMetric { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -46865,9 +46245,6 @@ impl serde::Serialize for VideoLayer { if !self.rid.is_empty() { len += 1; } - if self.repair_ssrc != 0 { - len += 1; - } let mut struct_ser = serializer.serialize_struct("livekit.VideoLayer", len)?; if self.quality != 0 { let v = VideoQuality::try_from(self.quality) @@ -46892,9 +46269,6 @@ impl serde::Serialize for VideoLayer { if !self.rid.is_empty() { struct_ser.serialize_field("rid", &self.rid)?; } - if self.repair_ssrc != 0 { - struct_ser.serialize_field("repairSsrc", &self.repair_ssrc)?; - } struct_ser.end() } } @@ -46913,8 +46287,6 @@ impl<'de> serde::Deserialize<'de> for VideoLayer { "spatial_layer", "spatialLayer", "rid", - "repair_ssrc", - "repairSsrc", ]; #[allow(clippy::enum_variant_names)] @@ -46926,7 +46298,6 @@ impl<'de> serde::Deserialize<'de> for VideoLayer { Ssrc, SpatialLayer, Rid, - RepairSsrc, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -46956,7 +46327,6 @@ impl<'de> serde::Deserialize<'de> for VideoLayer { "ssrc" => Ok(GeneratedField::Ssrc), "spatialLayer" | "spatial_layer" => Ok(GeneratedField::SpatialLayer), "rid" => Ok(GeneratedField::Rid), - "repairSsrc" | "repair_ssrc" => Ok(GeneratedField::RepairSsrc), _ => Ok(GeneratedField::__SkipField__), } } @@ -46983,7 +46353,6 @@ impl<'de> serde::Deserialize<'de> for VideoLayer { let mut ssrc__ = None; let mut spatial_layer__ = None; let mut rid__ = None; - let mut repair_ssrc__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Quality => { @@ -47038,14 +46407,6 @@ impl<'de> serde::Deserialize<'de> for VideoLayer { } rid__ = Some(map_.next_value()?); } - GeneratedField::RepairSsrc => { - if repair_ssrc__.is_some() { - return Err(serde::de::Error::duplicate_field("repairSsrc")); - } - repair_ssrc__ = - Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) - ; - } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -47059,7 +46420,6 @@ impl<'de> serde::Deserialize<'de> for VideoLayer { ssrc: ssrc__.unwrap_or_default(), spatial_layer: spatial_layer__.unwrap_or_default(), rid: rid__.unwrap_or_default(), - repair_ssrc: repair_ssrc__.unwrap_or_default(), }) } } @@ -48135,12 +47495,6 @@ impl serde::Serialize for WorkerMessage { worker_message::Message::MigrateJob(v) => { struct_ser.serialize_field("migrateJob", v)?; } - worker_message::Message::TextResponse(v) => { - struct_ser.serialize_field("textResponse", v)?; - } - worker_message::Message::PushText(v) => { - struct_ser.serialize_field("pushText", v)?; - } } } struct_ser.end() @@ -48164,10 +47518,6 @@ impl<'de> serde::Deserialize<'de> for WorkerMessage { "simulateJob", "migrate_job", "migrateJob", - "text_response", - "textResponse", - "push_text", - "pushText", ]; #[allow(clippy::enum_variant_names)] @@ -48179,8 +47529,6 @@ impl<'de> serde::Deserialize<'de> for WorkerMessage { Ping, SimulateJob, MigrateJob, - TextResponse, - PushText, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -48210,8 +47558,6 @@ impl<'de> serde::Deserialize<'de> for WorkerMessage { "ping" => Ok(GeneratedField::Ping), "simulateJob" | "simulate_job" => Ok(GeneratedField::SimulateJob), "migrateJob" | "migrate_job" => Ok(GeneratedField::MigrateJob), - "textResponse" | "text_response" => Ok(GeneratedField::TextResponse), - "pushText" | "push_text" => Ok(GeneratedField::PushText), _ => Ok(GeneratedField::__SkipField__), } } @@ -48281,20 +47627,6 @@ impl<'de> serde::Deserialize<'de> for WorkerMessage { return Err(serde::de::Error::duplicate_field("migrateJob")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(worker_message::Message::MigrateJob) -; - } - GeneratedField::TextResponse => { - if message__.is_some() { - return Err(serde::de::Error::duplicate_field("textResponse")); - } - message__ = map_.next_value::<::std::option::Option<_>>()?.map(worker_message::Message::TextResponse) -; - } - GeneratedField::PushText => { - if message__.is_some() { - return Err(serde::de::Error::duplicate_field("pushText")); - } - message__ = map_.next_value::<::std::option::Option<_>>()?.map(worker_message::Message::PushText) ; } GeneratedField::__SkipField__ => {